Before big moderation merge

This commit is contained in:
Dessalines 2019-04-16 16:04:23 -07:00
parent 5351e379d5
commit 2e73146e50
38 changed files with 1126 additions and 219 deletions

View file

@ -19,12 +19,11 @@ Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Infern
## Features
- TBD
-
the name
Lead singer from motorhead.
The old school video game.
The furry rodents.
## Why's it called Lemmy?
- Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
- The old school [video game](https://en.wikipedia.org/wiki/Lemmings_(video_game)).
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
Goals r/ censorship

View file

@ -6,8 +6,8 @@ create table user_ (
password_encrypted text not null,
email text unique,
icon bytea,
admin boolean default false,
banned boolean default false,
admin boolean default false not null,
banned boolean default false not null,
published timestamp not null default now(),
updated timestamp,
unique(name, fedi_name)

View file

@ -1,3 +1,4 @@
drop table site;
drop table community_user_ban;;
drop table community_moderator;
drop table community_follower;

View file

@ -68,3 +68,12 @@ create table community_user_ban (
);
insert into community (name, title, category_id, creator_id) values ('main', 'The Default Community', 1, 1);
create table site (
id serial primary key,
name varchar(20) not null unique,
description text,
creator_id int references user_ on update cascade on delete cascade not null,
published timestamp not null default now(),
updated timestamp
);

View file

@ -2,3 +2,4 @@ drop view community_view;
drop view community_moderator_view;
drop view community_follower_view;
drop view community_user_ban_view;
drop view site_view;

View file

@ -46,3 +46,11 @@ select *,
(select name from user_ u where cm.user_id = u.id) as user_name,
(select name from community c where cm.community_id = c.id) as community_name
from community_user_ban cm;
create view site_view as
select *,
(select name from user_ u where s.creator_id = u.id) as creator_name,
(select count(*) from user_) as number_of_users,
(select count(*) from post) as number_of_posts,
(select count(*) from comment) as number_of_comments
from site s;

View file

@ -137,8 +137,8 @@ mod tests {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};

View file

@ -138,8 +138,8 @@ mod tests {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};

View file

@ -1,5 +1,5 @@
extern crate diesel;
use schema::{community, community_moderator, community_follower, community_user_ban};
use schema::{community, community_moderator, community_follower, community_user_ban, site};
use diesel::*;
use diesel::result::Error;
use serde::{Deserialize, Serialize};
@ -31,58 +31,6 @@ pub struct CommunityForm {
pub updated: Option<chrono::NaiveDateTime>
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_moderator"]
pub struct CommunityModerator {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_moderator"]
pub struct CommunityModeratorForm {
pub community_id: i32,
pub user_id: i32,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_user_ban"]
pub struct CommunityUserBan {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_user_ban"]
pub struct CommunityUserBanForm {
pub community_id: i32,
pub user_id: i32,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_follower"]
pub struct CommunityFollower {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_follower"]
pub struct CommunityFollowerForm {
pub community_id: i32,
pub user_id: i32,
}
impl Crud<CommunityForm> for Community {
fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
use schema::community::dsl::*;
@ -111,20 +59,21 @@ impl Crud<CommunityForm> for Community {
}
}
impl Followable<CommunityFollowerForm> for CommunityFollower {
fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> {
use schema::community_follower::dsl::*;
insert_into(community_follower)
.values(community_follower_form)
.get_result::<Self>(conn)
}
fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<usize, Error> {
use schema::community_follower::dsl::*;
diesel::delete(community_follower
.filter(community_id.eq(&community_follower_form.community_id))
.filter(user_id.eq(&community_follower_form.user_id)))
.execute(conn)
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_moderator"]
pub struct CommunityModerator {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_moderator"]
pub struct CommunityModeratorForm {
pub community_id: i32,
pub user_id: i32,
}
impl Joinable<CommunityModeratorForm> for CommunityModerator {
@ -144,6 +93,23 @@ impl Joinable<CommunityModeratorForm> for CommunityModerator {
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_user_ban"]
pub struct CommunityUserBan {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_user_ban"]
pub struct CommunityUserBanForm {
pub community_id: i32,
pub user_id: i32,
}
impl Bannable<CommunityUserBanForm> for CommunityUserBan {
fn ban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<Self, Error> {
use schema::community_user_ban::dsl::*;
@ -161,6 +127,86 @@ impl Bannable<CommunityUserBanForm> for CommunityUserBan {
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_follower"]
pub struct CommunityFollower {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_follower"]
pub struct CommunityFollowerForm {
pub community_id: i32,
pub user_id: i32,
}
impl Followable<CommunityFollowerForm> for CommunityFollower {
fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> {
use schema::community_follower::dsl::*;
insert_into(community_follower)
.values(community_follower_form)
.get_result::<Self>(conn)
}
fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<usize, Error> {
use schema::community_follower::dsl::*;
diesel::delete(community_follower
.filter(community_id.eq(&community_follower_form.community_id))
.filter(user_id.eq(&community_follower_form.user_id)))
.execute(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="site"]
pub struct Site {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="site"]
pub struct SiteForm {
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub updated: Option<chrono::NaiveDateTime>
}
impl Crud<SiteForm> for Site {
fn read(conn: &PgConnection, _site_id: i32) -> Result<Self, Error> {
use schema::site::dsl::*;
site.first::<Self>(conn)
}
fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
use schema::site::dsl::*;
diesel::delete(site.find(site_id))
.execute(conn)
}
fn create(conn: &PgConnection, new_site: &SiteForm) -> Result<Self, Error> {
use schema::site::dsl::*;
insert_into(site)
.values(new_site)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, site_id: i32, new_site: &SiteForm) -> Result<Self, Error> {
use schema::site::dsl::*;
diesel::update(site.find(site_id))
.set(new_site)
.get_result::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use establish_connection;
@ -177,8 +223,8 @@ mod tests {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};

View file

@ -59,6 +59,21 @@ table! {
}
}
table! {
site_view (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
creator_name -> Varchar,
number_of_users -> BigInt,
number_of_posts -> BigInt,
number_of_comments -> BigInt,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="community_view"]
pub struct CommunityView {
@ -204,3 +219,26 @@ impl CommunityUserBanView {
.first::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="site_view"]
pub struct SiteView {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub creator_name: String,
pub number_of_users: i64,
pub number_of_posts: i64,
pub number_of_comments: i64,
}
impl SiteView {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
use actions::community_view::site_view::dsl::*;
site_view.first::<Self>(conn)
}
}

View file

@ -415,8 +415,8 @@ mod tests {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};
@ -428,8 +428,8 @@ mod tests {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};

View file

@ -119,8 +119,8 @@ mod tests {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};

View file

@ -165,8 +165,8 @@ mod tests {
password_encrypted: "nope".into(),
email: None,
updated: None,
admin: None,
banned: None,
admin: false,
banned: false,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -17,8 +17,8 @@ pub struct User_ {
pub password_encrypted: String,
pub email: Option<String>,
pub icon: Option<Vec<u8>>,
pub admin: Option<bool>,
pub banned: Option<bool>,
pub admin: bool,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>
}
@ -30,8 +30,8 @@ pub struct UserForm {
pub fedi_name: String,
pub preferred_username: Option<String>,
pub password_encrypted: String,
pub admin: Option<bool>,
pub banned: Option<bool>,
pub admin: bool,
pub banned: bool,
pub email: Option<String>,
pub updated: Option<chrono::NaiveDateTime>
}
@ -46,22 +46,26 @@ impl Crud<UserForm> for User_ {
.execute(conn)
}
fn create(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
let mut edited_user = form.clone();
let password_hash = hash(&form.password_encrypted, DEFAULT_COST)
.expect("Couldn't hash password");
edited_user.password_encrypted = password_hash;
insert_into(user_)
.values(edited_user)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, user_id: i32, form: &UserForm) -> Result<Self, Error> {
diesel::update(user_.find(user_id))
.set(form)
.get_result::<Self>(conn)
}
}
impl User_ {
pub fn register(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
let mut edited_user = form.clone();
let password_hash = hash(&form.password_encrypted, DEFAULT_COST)
.expect("Couldn't hash password");
edited_user.password_encrypted = password_hash;
diesel::update(user_.find(user_id))
.set(edited_user)
.get_result::<Self>(conn)
Self::create(&conn, &edited_user)
}
}
@ -126,8 +130,8 @@ mod tests {
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};
@ -138,11 +142,11 @@ mod tests {
name: "thommy".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "$2y$12$YXpNpYsdfjmed.QlYLvw4OfTCgyKUnKHc/V8Dgcf9YcVKHPaYXYYy".into(),
password_encrypted: "nope".into(),
email: None,
icon: None,
admin: Some(false),
banned: Some(false),
admin: false,
banned: false,
published: inserted_user.published,
updated: None
};
@ -151,9 +155,9 @@ mod tests {
let updated_user = User_::update(&conn, inserted_user.id, &new_user).unwrap();
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(expected_user.id, read_user.id);
assert_eq!(expected_user.id, inserted_user.id);
assert_eq!(expected_user.id, updated_user.id);
assert_eq!(expected_user, read_user);
assert_eq!(expected_user, inserted_user);
assert_eq!(expected_user, updated_user);
assert_eq!(1, num_deleted);
}
}

View file

@ -8,8 +8,8 @@ table! {
id -> Int4,
name -> Varchar,
fedi_name -> Varchar,
admin -> Nullable<Bool>,
banned -> Nullable<Bool>,
admin -> Bool,
banned -> Bool,
published -> Timestamp,
number_of_posts -> BigInt,
post_score -> BigInt,
@ -24,8 +24,8 @@ pub struct UserView {
pub id: i32,
pub name: String,
pub fedi_name: String,
pub admin: Option<bool>,
pub banned: Option<bool>,
pub admin: bool,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub number_of_posts: i64,
pub post_score: i64,
@ -40,5 +40,17 @@ impl UserView {
user_view.find(from_user_id)
.first::<Self>(conn)
}
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use actions::user_view::user_view::dsl::*;
user_view.filter(admin.eq(true))
.load::<Self>(conn)
}
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use actions::user_view::user_view::dsl::*;
user_view.filter(banned.eq(true))
.load::<Self>(conn)
}
}

View file

@ -44,8 +44,8 @@ mod tests {
email: None,
icon: None,
published: naive_now(),
admin: None,
banned: None,
admin: false,
banned: false,
updated: None
};

View file

@ -185,6 +185,17 @@ table! {
}
}
table! {
site (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
}
table! {
user_ (id) {
id -> Int4,
@ -194,8 +205,8 @@ table! {
password_encrypted -> Text,
email -> Nullable<Text>,
icon -> Nullable<Bytea>,
admin -> Nullable<Bool>,
banned -> Nullable<Bool>,
admin -> Bool,
banned -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
@ -236,6 +247,7 @@ joinable!(post -> community (community_id));
joinable!(post -> user_ (creator_id));
joinable!(post_like -> post (post_id));
joinable!(post_like -> user_ (user_id));
joinable!(site -> user_ (creator_id));
joinable!(user_ban -> user_ (user_id));
allow_tables_to_appear_in_same_query!(
@ -256,6 +268,7 @@ allow_tables_to_appear_in_same_query!(
mod_remove_post,
post,
post_like,
site,
user_,
user_ban,
);

View file

@ -26,7 +26,7 @@ use actions::moderator::*;
#[derive(EnumString,ToString,Debug)]
pub enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity,
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
}
#[derive(Serialize, Deserialize)]
@ -88,7 +88,8 @@ pub struct Register {
username: String,
email: Option<String>,
password: String,
password_verify: String
password_verify: String,
admin: bool,
}
#[derive(Serialize, Deserialize)]
@ -361,6 +362,67 @@ pub struct AddModToCommunityResponse {
moderators: Vec<CommunityModeratorView>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateSite {
name: String,
description: Option<String>,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct EditSite {
name: String,
description: Option<String>,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct GetSite {
}
#[derive(Serialize, Deserialize)]
pub struct SiteResponse {
op: String,
site: SiteView,
}
#[derive(Serialize, Deserialize)]
pub struct GetSiteResponse {
op: String,
site: Option<SiteView>,
admins: Vec<UserView>,
banned: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
pub struct AddAdmin {
user_id: i32,
added: bool,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct AddAdminResponse {
op: String,
admins: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
pub struct BanUser {
user_id: i32,
ban: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct BanUserResponse {
op: String,
user: UserView,
banned: bool,
}
/// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session. implementation is super primitive
pub struct ChatServer {
@ -563,6 +625,26 @@ impl Handler<StandardMessage> for ChatServer {
let mod_add_to_community: AddModToCommunity = serde_json::from_str(data).unwrap();
mod_add_to_community.perform(self, msg.id)
},
UserOperation::CreateSite => {
let create_site: CreateSite = serde_json::from_str(data).unwrap();
create_site.perform(self, msg.id)
},
UserOperation::EditSite => {
let edit_site: EditSite = serde_json::from_str(data).unwrap();
edit_site.perform(self, msg.id)
},
UserOperation::GetSite => {
let get_site: GetSite = serde_json::from_str(data).unwrap();
get_site.perform(self, msg.id)
},
UserOperation::AddAdmin => {
let add_admin: AddAdmin = serde_json::from_str(data).unwrap();
add_admin.perform(self, msg.id)
},
UserOperation::BanUser => {
let ban_user: BanUser = serde_json::from_str(data).unwrap();
ban_user.perform(self, msg.id)
},
};
MessageResult(res)
@ -633,6 +715,11 @@ impl Perform for Register {
return self.error("No slurs");
}
// Make sure there are no admins
if self.admin && UserView::admins(&conn).unwrap().len() > 0 {
return self.error("Sorry, there's already an admin.");
}
// Register the new user
let user_form = UserForm {
name: self.username.to_owned(),
@ -641,18 +728,34 @@ impl Perform for Register {
password_encrypted: self.password.to_owned(),
preferred_username: None,
updated: None,
admin: None,
banned: None,
admin: self.admin,
banned: false,
};
// Create the user
let inserted_user = match User_::create(&conn, &user_form) {
let inserted_user = match User_::register(&conn, &user_form) {
Ok(user) => user,
Err(_e) => {
return self.error("User already exists.");
}
};
// If its an admin, add them as a mod to main
if self.admin {
let community_moderator_form = CommunityModeratorForm {
community_id: 1,
user_id: inserted_user.id
};
let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return self.error("Community moderator already exists.");
}
};
}
// Return the jwt
serde_json::to_string(
&LoginResponse {
@ -1852,3 +1955,284 @@ impl Perform for AddModToCommunity {
}
}
impl Perform for CreateSite {
fn op_type(&self) -> UserOperation {
UserOperation::CreateSite
}
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
let conn = establish_connection();
let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return self.error("Not logged in.");
}
};
if has_slurs(&self.name) ||
(self.description.is_some() && has_slurs(&self.description.to_owned().unwrap())) {
return self.error("No slurs");
}
let user_id = claims.id;
// Make sure user is an admin
if !UserView::read(&conn, user_id).unwrap().admin {
return self.error("Not an admin.");
}
let site_form = SiteForm {
name: self.name.to_owned(),
description: self.description.to_owned(),
creator_id: user_id,
updated: None,
};
match Site::create(&conn, &site_form) {
Ok(site) => site,
Err(_e) => {
return self.error("Site exists already");
}
};
let site_view = SiteView::read(&conn).unwrap();
serde_json::to_string(
&SiteResponse {
op: self.op_type().to_string(),
site: site_view,
}
)
.unwrap()
}
}
impl Perform for EditSite {
fn op_type(&self) -> UserOperation {
UserOperation::EditSite
}
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
let conn = establish_connection();
let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return self.error("Not logged in.");
}
};
if has_slurs(&self.name) ||
(self.description.is_some() && has_slurs(&self.description.to_owned().unwrap())) {
return self.error("No slurs");
}
let user_id = claims.id;
// Make sure user is an admin
if UserView::read(&conn, user_id).unwrap().admin == false {
return self.error("Not an admin.");
}
let found_site = Site::read(&conn, 1).unwrap();
let site_form = SiteForm {
name: self.name.to_owned(),
description: self.description.to_owned(),
creator_id: found_site.creator_id,
updated: Some(naive_now()),
};
match Site::update(&conn, 1, &site_form) {
Ok(site) => site,
Err(_e) => {
return self.error("Couldn't update site.");
}
};
let site_view = SiteView::read(&conn).unwrap();
serde_json::to_string(
&SiteResponse {
op: self.op_type().to_string(),
site: site_view,
}
)
.unwrap()
}
}
impl Perform for GetSite {
fn op_type(&self) -> UserOperation {
UserOperation::GetSite
}
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
let conn = establish_connection();
// It can return a null site in order to redirect
let site_view = match Site::read(&conn, 1) {
Ok(_site) => Some(SiteView::read(&conn).unwrap()),
Err(_e) => None
};
let admins = UserView::admins(&conn).unwrap();
let banned = UserView::banned(&conn).unwrap();
serde_json::to_string(
&GetSiteResponse {
op: self.op_type().to_string(),
site: site_view,
admins: admins,
banned: banned,
}
)
.unwrap()
}
}
impl Perform for AddAdmin {
fn op_type(&self) -> UserOperation {
UserOperation::AddAdmin
}
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
let conn = establish_connection();
let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return self.error("Not logged in.");
}
};
let user_id = claims.id;
// Make sure user is an admin
if UserView::read(&conn, user_id).unwrap().admin == false {
return self.error("Not an admin.");
}
let read_user = User_::read(&conn, self.user_id).unwrap();
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
password_encrypted: read_user.password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: self.added,
banned: read_user.banned,
};
match User_::update(&conn, self.user_id, &user_form) {
Ok(user) => user,
Err(_e) => {
return self.error("Couldn't update user");
}
};
// Mod tables
let form = ModAddForm {
mod_user_id: user_id,
other_user_id: self.user_id,
removed: Some(!self.added),
};
ModAdd::create(&conn, &form).unwrap();
let admins = UserView::admins(&conn).unwrap();
let res = serde_json::to_string(
&AddAdminResponse {
op: self.op_type().to_string(),
admins: admins,
}
)
.unwrap();
res
}
}
impl Perform for BanUser {
fn op_type(&self) -> UserOperation {
UserOperation::BanUser
}
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
let conn = establish_connection();
let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return self.error("Not logged in.");
}
};
let user_id = claims.id;
// Make sure user is an admin
if UserView::read(&conn, user_id).unwrap().admin == false {
return self.error("Not an admin.");
}
let read_user = User_::read(&conn, self.user_id).unwrap();
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
password_encrypted: read_user.password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: read_user.admin,
banned: self.ban,
};
match User_::update(&conn, self.user_id, &user_form) {
Ok(user) => user,
Err(_e) => {
return self.error("Couldn't update user");
}
};
// Mod tables
let expires = match self.expires {
Some(time) => Some(naive_from_unix(time)),
None => None
};
let form = ModBanForm {
mod_user_id: user_id,
other_user_id: self.user_id,
reason: self.reason.to_owned(),
banned: Some(self.ban),
expires: expires,
};
ModBan::create(&conn, &form).unwrap();
let user_view = UserView::read(&conn, self.user_id).unwrap();
let res = serde_json::to_string(
&BanUserResponse {
op: self.op_type().to_string(),
user: user_view,
banned: self.ban
}
)
.unwrap();
res
}
}

View file

@ -23,7 +23,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
auth: null,
content: null,
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
creator_id: UserService.Instance.loggedIn ? UserService.Instance.user.id : null,
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
},
buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply",
}
@ -71,6 +71,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
}
handleCommentSubmit(i: CommentForm, event: any) {
event.preventDefault();
if (i.props.edit) {
WebSocketService.Instance.editComment(i.state.commentForm);
} else {

View file

@ -154,13 +154,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
get myComment(): boolean {
return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id;
return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id;
}
get canMod(): boolean {
// You can do moderator actions only on the mods added after you.
if (UserService.Instance.loggedIn) {
if (UserService.Instance.user) {
let modIds = this.props.moderators.map(m => m.user_id);
let yourIndex = modIds.findIndex(id => id == UserService.Instance.user.id);
if (yourIndex == -1) {
@ -240,6 +240,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
handleModRemoveSubmit(i: CommentNode) {
event.preventDefault();
let form: CommentFormI = {
content: i.props.node.comment.content,
edit_id: i.props.node.comment.id,
@ -272,6 +273,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
handleModBanSubmit(i: CommentNode) {
event.preventDefault();
let form: BanFromCommunityForm = {
user_id: i.props.node.comment.creator_id,
community_id: i.props.node.comment.community_id,

View file

@ -7,7 +7,7 @@ interface CommentNodesState {
interface CommentNodesProps {
nodes: Array<CommentNodeI>;
moderators: Array<CommunityUser>;
moderators?: Array<CommunityUser>;
noIndent?: boolean;
viewOnly?: boolean;
locked?: boolean;

View file

@ -62,9 +62,9 @@ export class Communities extends Component<any, CommunitiesState> {
<th>Name</th>
<th>Title</th>
<th>Category</th>
<th class="text-right">Subscribers</th>
<th class="text-right">Posts</th>
<th class="text-right">Comments</th>
<th class="text-right d-none d-md-table-cell">Subscribers</th>
<th class="text-right d-none d-md-table-cell">Posts</th>
<th class="text-right d-none d-md-table-cell">Comments</th>
<th></th>
</tr>
</thead>
@ -74,13 +74,13 @@ export class Communities extends Component<any, CommunitiesState> {
<td><Link to={`/community/${community.id}`}>{community.name}</Link></td>
<td>{community.title}</td>
<td>{community.category_name}</td>
<td class="text-right">{community.number_of_subscribers}</td>
<td class="text-right">{community.number_of_posts}</td>
<td class="text-right">{community.number_of_comments}</td>
<td class="text-right d-none d-md-table-cell">{community.number_of_subscribers}</td>
<td class="text-right d-none d-md-table-cell">{community.number_of_posts}</td>
<td class="text-right d-none d-md-table-cell">{community.number_of_comments}</td>
<td class="text-right">
{community.subscribed ?
<button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button> :
<button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</span> :
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</span>
}
</td>
</tr>

View file

@ -0,0 +1,36 @@
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { repoUrl } from '../utils';
import { version } from '../version';
export class Footer extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<nav title={version} class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3 my-2">
<div className="navbar-collapse">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<Link class="nav-link" to="/modlog">Modlog</Link>
</li>
<li class="nav-item">
<a class="nav-link" href={repoUrl}>Contribute</a>
</li>
<li class="nav-item">
<a class="nav-link" href={repoUrl}>Code</a>
</li>
<li class="nav-item">
<a class="nav-link" href={repoUrl}>About</a>
</li>
</ul>
</div>
</nav>
);
}
}

View file

@ -20,7 +20,8 @@ let emptyState: State = {
registerForm: {
username: undefined,
password: undefined,
password_verify: undefined
password_verify: undefined,
admin: false,
},
loginLoading: false,
registerLoading: false
@ -147,6 +148,7 @@ export class Login extends Component<any, State> {
}
handleRegisterSubmit(i: Login, event: any) {
event.preventDefault();
i.state.registerLoading = true;
i.setState(i.state);
event.preventDefault();

View file

@ -2,14 +2,15 @@ import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType } from '../interfaces';
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings';
import { msgOp, repoUrl } from '../utils';
import { msgOp, repoUrl, mdToHtml } from '../utils';
interface State {
subscribedCommunities: Array<CommunityUser>;
trendingCommunities: Array<Community>;
site: GetSiteResponse;
loading: boolean;
}
@ -19,6 +20,21 @@ export class Main extends Component<any, State> {
private emptyState: State = {
subscribedCommunities: [],
trendingCommunities: [],
site: {
op: null,
site: {
id: null,
name: null,
creator_id: null,
creator_name: null,
published: null,
number_of_users: null,
number_of_posts: null,
number_of_comments: null,
},
admins: [],
banned: [],
},
loading: true
}
@ -35,7 +51,9 @@ export class Main extends Component<any, State> {
() => console.log('complete')
);
if (UserService.Instance.loggedIn) {
WebSocketService.Instance.getSite();
if (UserService.Instance.user) {
WebSocketService.Instance.getFollowedCommunities();
}
@ -63,7 +81,7 @@ export class Main extends Component<any, State> {
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div>
{this.trendingCommunities()}
{UserService.Instance.loggedIn ?
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div>
<h4>Subscribed forums</h4>
<ul class="list-inline">
@ -71,9 +89,9 @@ export class Main extends Component<any, State> {
<li class="list-inline-item"><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
)}
</ul>
</div> :
this.landing()
</div>
}
{this.landing()}
</div>
}
</div>
@ -85,7 +103,7 @@ export class Main extends Component<any, State> {
trendingCommunities() {
return (
<div>
<h4>Trending forums</h4>
<h4>Trending <Link class="text-white" to="/communities">forums</Link></h4>
<ul class="list-inline">
{this.state.trendingCommunities.map(community =>
<li class="list-inline-item"><Link to={`/community/${community.id}`}>{community.name}</Link></li>
@ -98,6 +116,26 @@ export class Main extends Component<any, State> {
landing() {
return (
<div>
<h4>{`${this.state.site.site.name}`}</h4>
<ul class="my-1 list-inline">
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_users} Users</li>
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li>
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_comments} Comments</li>
<li className="list-inline-item"><Link className="badge badge-light" to="/modlog">Modlog</Link></li>
</ul>
<ul class="list-inline small">
<li class="list-inline-item">admins: </li>
{this.state.site.admins.map(admin =>
<li class="list-inline-item"><Link class="text-info" to={`/user/${admin.id}`}>{admin.name}</Link></li>
)}
</ul>
{this.state.site.site.description &&
<div>
<hr />
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
<hr />
</div>
}
<h4>Welcome to
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
<a href={repoUrl}>Lemmy<sup>Beta</sup></a>
@ -127,6 +165,17 @@ export class Main extends Component<any, State> {
this.state.trendingCommunities = res.communities;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
// This means it hasn't been set up yet
if (!res.site) {
this.context.router.history.push("/setup");
}
this.state.site.admins = res.admins;
this.state.site.site = res.site;
this.state.site.banned = res.banned;
this.setState(this.state);
}
}
}

View file

@ -9,34 +9,24 @@ import { MomentTime } from './moment-time';
import * as moment from 'moment';
interface ModlogState {
removed_posts: Array<ModRemovePost>,
locked_posts: Array<ModLockPost>,
removed_comments: Array<ModRemoveComment>,
removed_communities: Array<ModRemoveCommunity>,
banned_from_community: Array<ModBanFromCommunity>,
banned: Array<ModBan>,
added_to_community: Array<ModAddCommunity>,
added: Array<ModAdd>,
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}>,
communityId?: number,
communityName?: string,
loading: boolean;
}
export class Modlog extends Component<any, ModlogState> {
private subscription: Subscription;
private emptyState: ModlogState = {
removed_posts: [],
locked_posts: [],
removed_comments: [],
removed_communities: [],
banned_from_community: [],
banned: [],
added_to_community: [],
added: [],
loading: true
combined: [],
loading: true,
}
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.state.communityId = this.props.match.params.community_id ? Number(this.props.match.params.community_id) : undefined;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
@ -46,7 +36,7 @@ export class Modlog extends Component<any, ModlogState> {
);
let modlogForm: GetModlogForm = {
community_id: this.state.communityId
};
WebSocketService.Instance.getModlog(modlogForm);
}
@ -55,30 +45,35 @@ export class Modlog extends Component<any, ModlogState> {
this.subscription.unsubscribe();
}
combined() {
let combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}> = [];
let removed_posts = addTypeInfo(this.state.removed_posts, "removed_posts");
let locked_posts = addTypeInfo(this.state.locked_posts, "locked_posts");
let removed_comments = addTypeInfo(this.state.removed_comments, "removed_comments");
let removed_communities = addTypeInfo(this.state.removed_communities, "removed_communities");
let banned_from_community = addTypeInfo(this.state.banned_from_community, "banned_from_community");
let added_to_community = addTypeInfo(this.state.added_to_community, "added_to_community");
setCombined(res: GetModlogResponse) {
let removed_posts = addTypeInfo(res.removed_posts, "removed_posts");
let locked_posts = addTypeInfo(res.locked_posts, "locked_posts");
let removed_comments = addTypeInfo(res.removed_comments, "removed_comments");
let removed_communities = addTypeInfo(res.removed_communities, "removed_communities");
let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community");
let added_to_community = addTypeInfo(res.added_to_community, "added_to_community");
combined.push(...removed_posts);
combined.push(...locked_posts);
combined.push(...removed_comments);
combined.push(...removed_communities);
combined.push(...banned_from_community);
combined.push(...added_to_community);
this.state.combined.push(...removed_posts);
this.state.combined.push(...locked_posts);
this.state.combined.push(...removed_comments);
this.state.combined.push(...removed_communities);
this.state.combined.push(...banned_from_community);
this.state.combined.push(...added_to_community);
if (this.state.communityId && this.state.combined.length > 0) {
this.state.communityName = this.state.combined[0].data.community_name;
}
// Sort them by time
combined.sort((a, b) => b.data.when_.localeCompare(a.data.when_));
this.state.combined.sort((a, b) => b.data.when_.localeCompare(a.data.when_));
console.log(combined);
this.setState(this.state);
}
combined() {
return (
<tbody>
{combined.map(i =>
{this.state.combined.map(i =>
<tr>
<td><MomentTime data={i.data} /></td>
<td><Link to={`/user/${i.data.mod_user_id}`}>{i.data.mod_user_name}</Link></td>
@ -143,7 +138,10 @@ export class Modlog extends Component<any, ModlogState> {
{this.state.loading ?
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div>
<h4>Modlog</h4>
<h4>
{this.state.communityName && <Link className="text-white" to={`/community/${this.state.communityId}`}>/f/{this.state.communityName} </Link>}
<span>Modlog</span>
</h4>
<div class="table-responsive">
<table id="modlog_table" class="table table-sm table-hover">
<thead class="pointer">
@ -171,14 +169,7 @@ export class Modlog extends Component<any, ModlogState> {
} else if (op == UserOperation.GetModlog) {
let res: GetModlogResponse = msg;
this.state.loading = false;
this.state.removed_posts = res.removed_posts;
this.state.locked_posts = res.locked_posts;
this.state.removed_comments = res.removed_comments;
this.state.removed_communities = res.removed_communities;
this.state.banned_from_community = res.banned_from_community;
this.state.added_to_community = res.added_to_community;
this.setState(this.state);
this.setCombined(res);
}
}
}

View file

@ -1,6 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { repoUrl } from '../utils';
import { UserService } from '../services';
import { version } from '../version';
@ -13,7 +12,7 @@ interface NavbarState {
export class Navbar extends Component<any, NavbarState> {
emptyState: NavbarState = {
isLoggedIn: UserService.Instance.loggedIn,
isLoggedIn: UserService.Instance.user !== undefined,
expanded: false,
expandUserDropdown: false
}
@ -50,9 +49,6 @@ export class Navbar extends Component<any, NavbarState> {
</button>
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href={repoUrl}>About</a>
</li>
<li class="nav-item">
<Link class="nav-link" to="/communities">Forums</Link>
</li>

View file

@ -27,7 +27,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
name: null,
auth: null,
community_id: null,
creator_id: UserService.Instance.loggedIn ? UserService.Instance.user.id : null
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
},
communities: [],
loading: false
@ -95,6 +95,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
</div>
</div>
{/* Cant change a community from an edit */}
{!this.props.post &&
<div class="form-group row">
<label class="col-sm-2 col-form-label">Forum</label>
<div class="col-sm-10">
@ -105,6 +107,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</select>
</div>
</div>
}
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2">

View file

@ -160,7 +160,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
private get myPost(): boolean {
return UserService.Instance.loggedIn && this.props.post.creator_id == UserService.Instance.user.id;
return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id;
}
handlePostLike(i: PostListing) {
@ -220,6 +220,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
handleModRemoveSubmit(i: PostListing) {
event.preventDefault();
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,

View file

@ -43,7 +43,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
sortType: SortType.Hot,
type_: this.props.communityId
? ListingType.Community
: UserService.Instance.loggedIn
: UserService.Instance.user
? ListingType.Subscribed
: ListingType.All,
loading: true
@ -86,7 +86,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
{this.state.posts.length > 0
? this.state.posts.map(post =>
<PostListing post={post} showCommunity={!this.props.communityId}/>)
: <div>No Listings. Subscribe to some <Link to="/communities">forums</Link>.</div>
: <div>No Listings. {!this.props.communityId && <span>Subscribe to some <Link to="/communities">forums</Link>.</span>}</div>
}
</div>
}
@ -109,7 +109,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
<option value={SortType.TopAll}>All</option>
</select>
{!this.props.communityId &&
UserService.Instance.loggedIn &&
UserService.Instance.user &&
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
<option disabled>Type</option>
<option value={ListingType.All}>All</option>

View file

@ -79,14 +79,14 @@ export class Post extends Component<any, PostState> {
{this.state.loading ?
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div class="row">
<div class="col-12 col-sm-8 col-lg-7 mb-3">
<div class="col-12 col-md-8 col-lg-7 mb-3">
<PostListing post={this.state.post} showBody showCommunity editable />
<div className="mb-2" />
<CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
{this.sortRadios()}
{this.commentsTree()}
</div>
<div class="col-12 col-sm-4 col-lg-3 mb-3">
<div class="col-12 col-md-4 col-lg-3 mb-3 d-none d-md-block">
{this.state.comments.length > 0 && this.newComments()}
</div>
<div class="col-12 col-sm-12 col-lg-2">

147
ui/src/components/setup.tsx Normal file
View file

@ -0,0 +1,147 @@
import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
import { SiteForm } from './site-form';
interface State {
userForm: RegisterForm;
doneRegisteringUser: boolean;
userLoading: boolean;
}
export class Setup extends Component<any, State> {
private subscription: Subscription;
private emptyState: State = {
userForm: {
username: undefined,
password: undefined,
password_verify: undefined,
admin: true,
},
doneRegisteringUser: false,
userLoading: false,
}
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
(msg) => this.parseMessage(msg),
(err) => console.error(err),
() => console.log("complete")
);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<div class="container">
<div class="row">
<div class="col-12 offset-lg-3 col-lg-6">
<h3>Lemmy Instance Setup</h3>
{!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />}
</div>
</div>
</div>
)
}
registerUser() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h4>Set up Site Administrator</h4>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" value={this.state.userForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} pattern="[a-zA-Z0-9_]+" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="email" class="form-control" placeholder="Optional" value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Verify Password</label>
<div class="col-sm-10">
<input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.userLoading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button>
</div>
</div>
</form>
);
}
handleRegisterSubmit(i: Setup, event: any) {
event.preventDefault();
i.state.userLoading = true;
i.setState(i.state);
event.preventDefault();
WebSocketService.Instance.register(i.state.userForm);
}
handleRegisterUsernameChange(i: Setup, event: any) {
i.state.userForm.username = event.target.value;
i.setState(i.state);
}
handleRegisterEmailChange(i: Setup, event: any) {
i.state.userForm.email = event.target.value;
i.setState(i.state);
}
handleRegisterPasswordChange(i: Setup, event: any) {
i.state.userForm.password = event.target.value;
i.setState(i.state);
}
handleRegisterPasswordVerifyChange(i: Setup, event: any) {
i.state.userForm.password_verify = event.target.value;
i.setState(i.state);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(msg.error);
this.state.userLoading = false;
this.setState(this.state);
return;
} else if (op == UserOperation.Register) {
this.state.userLoading = false;
this.state.doneRegisteringUser = true;
let res: LoginResponse = msg;
UserService.Instance.login(res);
console.log(res);
this.setState(this.state);
} else if (op == UserOperation.CreateSite) {
this.props.history.push('/');
}
}
}

View file

@ -87,11 +87,18 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</div>
</form>
}
<ul class="mt-1 list-inline">
<ul class="my-1 list-inline">
<li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li>
<li className="list-inline-item badge badge-light">{community.number_of_subscribers} Subscribers</li>
<li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li>
<li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li>
<li className="list-inline-item"><Link className="badge badge-light" to={`/modlog/community/${this.props.community.id}`}>Modlog</Link></li>
</ul>
<ul class="list-inline small">
<li class="list-inline-item">mods: </li>
{this.props.moderators.map(mod =>
<li class="list-inline-item"><Link class="text-info" to={`/user/${mod.user_id}`}>{mod.user_name}</Link></li>
)}
</ul>
<div>
{community.subscribed
@ -103,15 +110,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div>
<hr />
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
<hr />
</div>
}
<hr />
<h4>Moderators</h4>
<ul class="list-inline">
{this.props.moderators.map(mod =>
<li class="list-inline-item"><Link to={`/user/${mod.user_id}`}>{mod.user_name}</Link></li>
)}
</ul>
</div>
);
}
@ -152,7 +153,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}
private get amCreator(): boolean {
return UserService.Instance.loggedIn && this.props.community.creator_id == UserService.Instance.user.id;
return this.props.community.creator_id == UserService.Instance.user.id;
}
// private get amMod(): boolean {
@ -180,7 +181,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}
handleModRemoveSubmit(i: Sidebar) {
event.preventDefault();
let deleteForm: CommunityFormI = {
name: i.props.community.name,
title: i.props.community.title,

View file

@ -0,0 +1,86 @@
import { Component, linkEvent } from 'inferno';
import { Site, SiteForm as SiteFormI } from '../interfaces';
import { WebSocketService } from '../services';
import * as autosize from 'autosize';
interface SiteFormProps {
site?: Site; // If a site is given, that means this is an edit
onCancel?(): any;
}
interface SiteFormState {
siteForm: SiteFormI;
loading: boolean;
}
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private emptyState: SiteFormState ={
siteForm: {
name: null
},
loading: false
}
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
}
componentDidMount() {
autosize(document.querySelectorAll('textarea'));
}
render() {
return (
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
<h4>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h4>
<div class="form-group row">
<label class="col-12 col-form-label">Name</label>
<div class="col-12">
<input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} />
</div>
</div>
<div class="form-group row">
<label class="col-12 col-form-label">Sidebar</label>
<div class="col-12">
<textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} />
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
this.props.site ? 'Save' : 'Create'}</button>
{this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
</div>
</div>
</form>
);
}
handleCreateSiteSubmit(i: SiteForm, event: any) {
event.preventDefault();
i.state.loading = true;
if (i.props.site) {
WebSocketService.Instance.editSite(i.state.siteForm);
} else {
WebSocketService.Instance.createSite(i.state.siteForm);
}
i.setState(i.state);
}
handleSiteNameChange(i: SiteForm, event: any) {
i.state.siteForm.name = event.target.value;
i.setState(i.state);
}
handleSiteDescriptionChange(i: SiteForm, event: any) {
i.state.siteForm.description = event.target.value;
i.setState(i.state);
}
handleCancel(i: SiteForm) {
i.props.onCancel();
}
}

View file

@ -2,6 +2,7 @@ import { render, Component } from 'inferno';
import { HashRouter, Route, Switch } from 'inferno-router';
import { Navbar } from './components/navbar';
import { Footer } from './components/footer';
import { Home } from './components/home';
import { Login } from './components/login';
import { CreatePost } from './components/create-post';
@ -11,6 +12,7 @@ import { Community } from './components/community';
import { Communities } from './components/communities';
import { User } from './components/user';
import { Modlog } from './components/modlog';
import { Setup } from './components/setup';
import { Symbols } from './components/symbols';
import './main.css';
@ -43,10 +45,13 @@ class Index extends Component<any, any> {
<Route path={`/community/:id`} component={Community} />
<Route path={`/user/:id/:heading`} component={User} />
<Route path={`/user/:id`} component={User} />
<Route path={`/modlog/community/:community_id`} component={Modlog} />
<Route path={`/modlog`} component={Modlog} />
<Route path={`/setup`} component={Setup} />
</Switch>
<Symbols />
</div>
<Footer />
</HashRouter>
);
}

View file

@ -1,5 +1,5 @@
export enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
}
export enum CommentSortType {
@ -107,6 +107,19 @@ export interface Category {
name: string;
}
export interface Site {
id: number;
name: string;
description?: string;
creator_id: number;
published: string;
updated?: string;
creator_name: string;
number_of_users: number;
number_of_posts: number;
number_of_comments: number;
}
export interface FollowCommunityForm {
community_id: number;
follow: boolean;
@ -294,6 +307,7 @@ export interface RegisterForm {
email?: string;
password: string;
password_verify: string;
admin: boolean;
}
export interface LoginResponse {
@ -421,4 +435,49 @@ export interface CreatePostLikeResponse {
post: Post;
}
export interface SiteForm {
name: string;
description?: string,
removed?: boolean;
reason?: string;
expires?: number;
auth?: string;
}
export interface GetSiteResponse {
op: string;
site: Site;
admins: Array<UserView>;
banned: Array<UserView>;
}
export interface SiteResponse {
op: string;
site: Site;
}
export interface BanUserForm {
user_id: number;
ban: boolean;
reason?: string,
expires?: number,
auth?: string;
}
export interface BanUserResponse {
op: string;
user: UserView,
banned: boolean,
}
export interface AddAdminForm {
user_id: number;
added: boolean;
auth?: string;
}
export interface AddAdminResponse {
op: string;
admins: Array<UserView>;
}

View file

@ -31,10 +31,6 @@ export class UserService {
this.sub.next(undefined);
}
public get loggedIn(): boolean {
return this.user !== undefined;
}
public get auth(): string {
return Cookies.get("jwt");
}

View file

@ -1,5 +1,5 @@
import { wsUri } from '../env';
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm } from '../interfaces';
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, SiteForm, Site, UserView } from '../interfaces';
import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
@ -8,11 +8,16 @@ import { UserService } from './';
export class WebSocketService {
private static _instance: WebSocketService;
public subject: Subject<any>;
public instanceName: string;
public site: Site;
public admins: Array<UserView>;
public banned: Array<UserView>;
private constructor() {
this.subject = webSocket(wsUri);
// Even tho this isn't used, its necessary to not keep reconnecting
// Necessary to not keep reconnecting
this.subject
.pipe(retryWhen(errors => errors.pipe(delay(60000), take(999))))
.subscribe();
@ -125,6 +130,19 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
}
public createSite(siteForm: SiteForm) {
this.setAuth(siteForm);
this.subject.next(this.wsSendWrapper(UserOperation.CreateSite, siteForm));
}
public editSite(siteForm: SiteForm) {
this.setAuth(siteForm);
this.subject.next(this.wsSendWrapper(UserOperation.EditSite, siteForm));
}
public getSite() {
this.subject.next(this.wsSendWrapper(UserOperation.GetSite, {}));
}
private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data };
console.log(send);
@ -138,7 +156,6 @@ export class WebSocketService {
throw "Not logged in";
}
}
}
window.onbeforeunload = (() => {