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 ## Features
- TBD - TBD
-
the name
Lead singer from motorhead. ## Why's it called Lemmy?
The old school video game. - Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
The furry rodents. - 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 Goals r/ censorship

View file

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

View file

@ -1,3 +1,4 @@
drop table site;
drop table community_user_ban;; drop table community_user_ban;;
drop table community_moderator; drop table community_moderator;
drop table community_follower; 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); 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_moderator_view;
drop view community_follower_view; drop view community_follower_view;
drop view community_user_ban_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 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 (select name from community c where cm.community_id = c.id) as community_name
from community_user_ban cm; 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, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: None, admin: false,
banned: None, banned: false,
updated: None updated: None
}; };

View file

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

View file

@ -1,5 +1,5 @@
extern crate diesel; 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::*;
use diesel::result::Error; use diesel::result::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -31,58 +31,6 @@ pub struct CommunityForm {
pub updated: Option<chrono::NaiveDateTime> 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 { impl Crud<CommunityForm> for Community {
fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> { fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
use schema::community::dsl::*; use schema::community::dsl::*;
@ -111,20 +59,21 @@ impl Crud<CommunityForm> for Community {
} }
} }
impl Followable<CommunityFollowerForm> for CommunityFollower { #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> { #[belongs_to(Community)]
use schema::community_follower::dsl::*; #[table_name = "community_moderator"]
insert_into(community_follower) pub struct CommunityModerator {
.values(community_follower_form) pub id: i32,
.get_result::<Self>(conn) pub community_id: i32,
} pub user_id: i32,
fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<usize, Error> { pub published: chrono::NaiveDateTime,
use schema::community_follower::dsl::*; }
diesel::delete(community_follower
.filter(community_id.eq(&community_follower_form.community_id)) #[derive(Insertable, AsChangeset, Clone)]
.filter(user_id.eq(&community_follower_form.user_id))) #[table_name="community_moderator"]
.execute(conn) pub struct CommunityModeratorForm {
} pub community_id: i32,
pub user_id: i32,
} }
impl Joinable<CommunityModeratorForm> for CommunityModerator { 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 { impl Bannable<CommunityUserBanForm> for CommunityUserBan {
fn ban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<Self, Error> { fn ban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<Self, Error> {
use schema::community_user_ban::dsl::*; 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)] #[cfg(test)]
mod tests { mod tests {
use establish_connection; use establish_connection;
@ -177,8 +223,8 @@ mod tests {
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: None, admin: false,
banned: None, banned: false,
updated: None 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)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="community_view"] #[table_name="community_view"]
pub struct CommunityView { pub struct CommunityView {
@ -204,3 +219,26 @@ impl CommunityUserBanView {
.first::<Self>(conn) .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, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: None, admin: false,
banned: None, banned: false,
updated: None updated: None
}; };
@ -428,8 +428,8 @@ mod tests {
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: None, admin: false,
banned: None, banned: false,
updated: None updated: None
}; };

View file

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

View file

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

View file

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

View file

@ -8,8 +8,8 @@ table! {
id -> Int4, id -> Int4,
name -> Varchar, name -> Varchar,
fedi_name -> Varchar, fedi_name -> Varchar,
admin -> Nullable<Bool>, admin -> Bool,
banned -> Nullable<Bool>, banned -> Bool,
published -> Timestamp, published -> Timestamp,
number_of_posts -> BigInt, number_of_posts -> BigInt,
post_score -> BigInt, post_score -> BigInt,
@ -24,8 +24,8 @@ pub struct UserView {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub fedi_name: String, pub fedi_name: String,
pub admin: Option<bool>, pub admin: bool,
pub banned: Option<bool>, pub banned: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub number_of_posts: i64, pub number_of_posts: i64,
pub post_score: i64, pub post_score: i64,
@ -40,5 +40,17 @@ impl UserView {
user_view.find(from_user_id) user_view.find(from_user_id)
.first::<Self>(conn) .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, email: None,
icon: None, icon: None,
published: naive_now(), published: naive_now(),
admin: None, admin: false,
banned: None, banned: false,
updated: None 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! { table! {
user_ (id) { user_ (id) {
id -> Int4, id -> Int4,
@ -194,8 +205,8 @@ table! {
password_encrypted -> Text, password_encrypted -> Text,
email -> Nullable<Text>, email -> Nullable<Text>,
icon -> Nullable<Bytea>, icon -> Nullable<Bytea>,
admin -> Nullable<Bool>, admin -> Bool,
banned -> Nullable<Bool>, banned -> Bool,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
} }
@ -236,6 +247,7 @@ joinable!(post -> community (community_id));
joinable!(post -> user_ (creator_id)); joinable!(post -> user_ (creator_id));
joinable!(post_like -> post (post_id)); joinable!(post_like -> post (post_id));
joinable!(post_like -> user_ (user_id)); joinable!(post_like -> user_ (user_id));
joinable!(site -> user_ (creator_id));
joinable!(user_ban -> user_ (user_id)); joinable!(user_ban -> user_ (user_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
@ -256,6 +268,7 @@ allow_tables_to_appear_in_same_query!(
mod_remove_post, mod_remove_post,
post, post,
post_like, post_like,
site,
user_, user_,
user_ban, user_ban,
); );

View file

@ -26,7 +26,7 @@ use actions::moderator::*;
#[derive(EnumString,ToString,Debug)] #[derive(EnumString,ToString,Debug)]
pub enum UserOperation { 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)] #[derive(Serialize, Deserialize)]
@ -88,7 +88,8 @@ pub struct Register {
username: String, username: String,
email: Option<String>, email: Option<String>,
password: String, password: String,
password_verify: String password_verify: String,
admin: bool,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -361,6 +362,67 @@ pub struct AddModToCommunityResponse {
moderators: Vec<CommunityModeratorView>, 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 /// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session. implementation is super primitive /// session. implementation is super primitive
pub struct ChatServer { pub struct ChatServer {
@ -563,6 +625,26 @@ impl Handler<StandardMessage> for ChatServer {
let mod_add_to_community: AddModToCommunity = serde_json::from_str(data).unwrap(); let mod_add_to_community: AddModToCommunity = serde_json::from_str(data).unwrap();
mod_add_to_community.perform(self, msg.id) 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) MessageResult(res)
@ -633,6 +715,11 @@ impl Perform for Register {
return self.error("No slurs"); 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 // Register the new user
let user_form = UserForm { let user_form = UserForm {
name: self.username.to_owned(), name: self.username.to_owned(),
@ -641,18 +728,34 @@ impl Perform for Register {
password_encrypted: self.password.to_owned(), password_encrypted: self.password.to_owned(),
preferred_username: None, preferred_username: None,
updated: None, updated: None,
admin: None, admin: self.admin,
banned: None, banned: false,
}; };
// Create the user // Create the user
let inserted_user = match User_::create(&conn, &user_form) { let inserted_user = match User_::register(&conn, &user_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return self.error("User already exists."); 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 // Return the jwt
serde_json::to_string( serde_json::to_string(
&LoginResponse { &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, auth: null,
content: null, content: null,
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId, 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", 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) { handleCommentSubmit(i: CommentForm, event: any) {
event.preventDefault();
if (i.props.edit) { if (i.props.edit) {
WebSocketService.Instance.editComment(i.state.commentForm); WebSocketService.Instance.editComment(i.state.commentForm);
} else { } else {

View file

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

View file

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

View file

@ -62,9 +62,9 @@ export class Communities extends Component<any, CommunitiesState> {
<th>Name</th> <th>Name</th>
<th>Title</th> <th>Title</th>
<th>Category</th> <th>Category</th>
<th class="text-right">Subscribers</th> <th class="text-right d-none d-md-table-cell">Subscribers</th>
<th class="text-right">Posts</th> <th class="text-right d-none d-md-table-cell">Posts</th>
<th class="text-right">Comments</th> <th class="text-right d-none d-md-table-cell">Comments</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -74,13 +74,13 @@ export class Communities extends Component<any, CommunitiesState> {
<td><Link to={`/community/${community.id}`}>{community.name}</Link></td> <td><Link to={`/community/${community.id}`}>{community.name}</Link></td>
<td>{community.title}</td> <td>{community.title}</td>
<td>{community.category_name}</td> <td>{community.category_name}</td>
<td class="text-right">{community.number_of_subscribers}</td> <td class="text-right d-none d-md-table-cell">{community.number_of_subscribers}</td>
<td class="text-right">{community.number_of_posts}</td> <td class="text-right d-none d-md-table-cell">{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_comments}</td>
<td class="text-right"> <td class="text-right">
{community.subscribed ? {community.subscribed ?
<button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button> : <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</span> :
<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.handleSubscribe)}>Subscribe</span>
} }
</td> </td>
</tr> </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: { registerForm: {
username: undefined, username: undefined,
password: undefined, password: undefined,
password_verify: undefined password_verify: undefined,
admin: false,
}, },
loginLoading: false, loginLoading: false,
registerLoading: false registerLoading: false
@ -147,6 +148,7 @@ export class Login extends Component<any, State> {
} }
handleRegisterSubmit(i: Login, event: any) { handleRegisterSubmit(i: Login, event: any) {
event.preventDefault();
i.state.registerLoading = true; i.state.registerLoading = true;
i.setState(i.state); i.setState(i.state);
event.preventDefault(); event.preventDefault();

View file

@ -2,14 +2,15 @@ import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; 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 { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { msgOp, repoUrl } from '../utils'; import { msgOp, repoUrl, mdToHtml } from '../utils';
interface State { interface State {
subscribedCommunities: Array<CommunityUser>; subscribedCommunities: Array<CommunityUser>;
trendingCommunities: Array<Community>; trendingCommunities: Array<Community>;
site: GetSiteResponse;
loading: boolean; loading: boolean;
} }
@ -19,6 +20,21 @@ export class Main extends Component<any, State> {
private emptyState: State = { private emptyState: State = {
subscribedCommunities: [], subscribedCommunities: [],
trendingCommunities: [], 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 loading: true
} }
@ -35,7 +51,9 @@ export class Main extends Component<any, State> {
() => console.log('complete') () => console.log('complete')
); );
if (UserService.Instance.loggedIn) { WebSocketService.Instance.getSite();
if (UserService.Instance.user) {
WebSocketService.Instance.getFollowedCommunities(); 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> : <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div> <div>
{this.trendingCommunities()} {this.trendingCommunities()}
{UserService.Instance.loggedIn ? {UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div> <div>
<h4>Subscribed forums</h4> <h4>Subscribed forums</h4>
<ul class="list-inline"> <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> <li class="list-inline-item"><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
)} )}
</ul> </ul>
</div> : </div>
this.landing()
} }
{this.landing()}
</div> </div>
} }
</div> </div>
@ -85,7 +103,7 @@ export class Main extends Component<any, State> {
trendingCommunities() { trendingCommunities() {
return ( return (
<div> <div>
<h4>Trending forums</h4> <h4>Trending <Link class="text-white" to="/communities">forums</Link></h4>
<ul class="list-inline"> <ul class="list-inline">
{this.state.trendingCommunities.map(community => {this.state.trendingCommunities.map(community =>
<li class="list-inline-item"><Link to={`/community/${community.id}`}>{community.name}</Link></li> <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() { landing() {
return ( return (
<div> <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 <h4>Welcome to
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg> <svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
<a href={repoUrl}>Lemmy<sup>Beta</sup></a> <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.trendingCommunities = res.communities;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); 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'; import * as moment from 'moment';
interface ModlogState { interface ModlogState {
removed_posts: Array<ModRemovePost>, combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}>,
locked_posts: Array<ModLockPost>, communityId?: number,
removed_comments: Array<ModRemoveComment>, communityName?: string,
removed_communities: Array<ModRemoveCommunity>,
banned_from_community: Array<ModBanFromCommunity>,
banned: Array<ModBan>,
added_to_community: Array<ModAddCommunity>,
added: Array<ModAdd>,
loading: boolean; loading: boolean;
} }
export class Modlog extends Component<any, ModlogState> { export class Modlog extends Component<any, ModlogState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: ModlogState = { private emptyState: ModlogState = {
removed_posts: [], combined: [],
locked_posts: [], loading: true,
removed_comments: [],
removed_communities: [],
banned_from_community: [],
banned: [],
added_to_community: [],
added: [],
loading: true
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState; 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 this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe( .subscribe(
@ -46,7 +36,7 @@ export class Modlog extends Component<any, ModlogState> {
); );
let modlogForm: GetModlogForm = { let modlogForm: GetModlogForm = {
community_id: this.state.communityId
}; };
WebSocketService.Instance.getModlog(modlogForm); WebSocketService.Instance.getModlog(modlogForm);
} }
@ -55,30 +45,35 @@ export class Modlog extends Component<any, ModlogState> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
combined() { setCombined(res: GetModlogResponse) {
let combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}> = []; let removed_posts = addTypeInfo(res.removed_posts, "removed_posts");
let removed_posts = addTypeInfo(this.state.removed_posts, "removed_posts"); let locked_posts = addTypeInfo(res.locked_posts, "locked_posts");
let locked_posts = addTypeInfo(this.state.locked_posts, "locked_posts"); let removed_comments = addTypeInfo(res.removed_comments, "removed_comments");
let removed_comments = addTypeInfo(this.state.removed_comments, "removed_comments"); let removed_communities = addTypeInfo(res.removed_communities, "removed_communities");
let removed_communities = addTypeInfo(this.state.removed_communities, "removed_communities"); let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community");
let banned_from_community = addTypeInfo(this.state.banned_from_community, "banned_from_community"); let added_to_community = addTypeInfo(res.added_to_community, "added_to_community");
let added_to_community = addTypeInfo(this.state.added_to_community, "added_to_community");
combined.push(...removed_posts); this.state.combined.push(...removed_posts);
combined.push(...locked_posts); this.state.combined.push(...locked_posts);
combined.push(...removed_comments); this.state.combined.push(...removed_comments);
combined.push(...removed_communities); this.state.combined.push(...removed_communities);
combined.push(...banned_from_community); this.state.combined.push(...banned_from_community);
combined.push(...added_to_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 // 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 ( return (
<tbody> <tbody>
{combined.map(i => {this.state.combined.map(i =>
<tr> <tr>
<td><MomentTime data={i.data} /></td> <td><MomentTime data={i.data} /></td>
<td><Link to={`/user/${i.data.mod_user_id}`}>{i.data.mod_user_name}</Link></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 ? {this.state.loading ?
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : <h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div> <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"> <div class="table-responsive">
<table id="modlog_table" class="table table-sm table-hover"> <table id="modlog_table" class="table table-sm table-hover">
<thead class="pointer"> <thead class="pointer">
@ -171,14 +169,7 @@ export class Modlog extends Component<any, ModlogState> {
} else if (op == UserOperation.GetModlog) { } else if (op == UserOperation.GetModlog) {
let res: GetModlogResponse = msg; let res: GetModlogResponse = msg;
this.state.loading = false; this.state.loading = false;
this.state.removed_posts = res.removed_posts; this.setCombined(res);
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);
} }
} }
} }

View file

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

View file

@ -27,7 +27,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
name: null, name: null,
auth: null, auth: null,
community_id: 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: [], communities: [],
loading: false loading: false
@ -95,7 +95,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} /> <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
</div> </div>
</div> </div>
<div class="form-group row"> {/* 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> <label class="col-sm-2 col-form-label">Forum</label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}> <select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
@ -104,7 +106,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
)} )}
</select> </select>
</div> </div>
</div> </div>
}
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2"> <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 { 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) { handlePostLike(i: PostListing) {
@ -220,6 +220,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handleModRemoveSubmit(i: PostListing) { handleModRemoveSubmit(i: PostListing) {
event.preventDefault();
let form: PostFormI = { let form: PostFormI = {
name: i.props.post.name, name: i.props.post.name,
community_id: i.props.post.community_id, community_id: i.props.post.community_id,

View file

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

View file

@ -79,14 +79,14 @@ export class Post extends Component<any, PostState> {
{this.state.loading ? {this.state.loading ?
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div class="row"> <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 /> <PostListing post={this.state.post} showBody showCommunity editable />
<div className="mb-2" /> <div className="mb-2" />
<CommentForm postId={this.state.post.id} disabled={this.state.post.locked} /> <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
{this.sortRadios()} {this.sortRadios()}
{this.commentsTree()} {this.commentsTree()}
</div> </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()} {this.state.comments.length > 0 && this.newComments()}
</div> </div>
<div class="col-12 col-sm-12 col-lg-2"> <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> </div>
</form> </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"><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_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_posts} Posts</li>
<li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</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> </ul>
<div> <div>
{community.subscribed {community.subscribed
@ -103,15 +110,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div> <div>
<hr /> <hr />
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} /> <div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
<hr />
</div> </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> </div>
); );
} }
@ -152,7 +153,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
private get amCreator(): boolean { 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 { // private get amMod(): boolean {
@ -180,7 +181,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
handleModRemoveSubmit(i: Sidebar) { handleModRemoveSubmit(i: Sidebar) {
event.preventDefault();
let deleteForm: CommunityFormI = { let deleteForm: CommunityFormI = {
name: i.props.community.name, name: i.props.community.name,
title: i.props.community.title, 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 { HashRouter, Route, Switch } from 'inferno-router';
import { Navbar } from './components/navbar'; import { Navbar } from './components/navbar';
import { Footer } from './components/footer';
import { Home } from './components/home'; import { Home } from './components/home';
import { Login } from './components/login'; import { Login } from './components/login';
import { CreatePost } from './components/create-post'; import { CreatePost } from './components/create-post';
@ -11,6 +12,7 @@ import { Community } from './components/community';
import { Communities } from './components/communities'; import { Communities } from './components/communities';
import { User } from './components/user'; import { User } from './components/user';
import { Modlog } from './components/modlog'; import { Modlog } from './components/modlog';
import { Setup } from './components/setup';
import { Symbols } from './components/symbols'; import { Symbols } from './components/symbols';
import './main.css'; import './main.css';
@ -43,10 +45,13 @@ class Index extends Component<any, any> {
<Route path={`/community/:id`} component={Community} /> <Route path={`/community/:id`} component={Community} />
<Route path={`/user/:id/:heading`} component={User} /> <Route path={`/user/:id/:heading`} component={User} />
<Route path={`/user/:id`} component={User} /> <Route path={`/user/:id`} component={User} />
<Route path={`/modlog/community/:community_id`} component={Modlog} />
<Route path={`/modlog`} component={Modlog} /> <Route path={`/modlog`} component={Modlog} />
<Route path={`/setup`} component={Setup} />
</Switch> </Switch>
<Symbols /> <Symbols />
</div> </div>
<Footer />
</HashRouter> </HashRouter>
); );
} }

View file

@ -1,5 +1,5 @@
export enum UserOperation { 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 { export enum CommentSortType {
@ -107,6 +107,19 @@ export interface Category {
name: string; 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 { export interface FollowCommunityForm {
community_id: number; community_id: number;
follow: boolean; follow: boolean;
@ -294,6 +307,7 @@ export interface RegisterForm {
email?: string; email?: string;
password: string; password: string;
password_verify: string; password_verify: string;
admin: boolean;
} }
export interface LoginResponse { export interface LoginResponse {
@ -421,4 +435,49 @@ export interface CreatePostLikeResponse {
post: Post; 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); this.sub.next(undefined);
} }
public get loggedIn(): boolean {
return this.user !== undefined;
}
public get auth(): string { public get auth(): string {
return Cookies.get("jwt"); return Cookies.get("jwt");
} }

View file

@ -1,5 +1,5 @@
import { wsUri } from '../env'; 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 { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
@ -8,16 +8,21 @@ import { UserService } from './';
export class WebSocketService { export class WebSocketService {
private static _instance: WebSocketService; private static _instance: WebSocketService;
public subject: Subject<any>; public subject: Subject<any>;
public instanceName: string;
public site: Site;
public admins: Array<UserView>;
public banned: Array<UserView>;
private constructor() { private constructor() {
this.subject = webSocket(wsUri); this.subject = webSocket(wsUri);
// Even tho this isn't used, its necessary to not keep reconnecting // Necessary to not keep reconnecting
this.subject this.subject
.pipe(retryWhen(errors => errors.pipe(delay(60000), take(999)))) .pipe(retryWhen(errors => errors.pipe(delay(60000), take(999))))
.subscribe(); .subscribe();
console.log(`Connected to ${wsUri}`); console.log(`Connected to ${wsUri}`);
} }
public static get Instance(){ public static get Instance(){
@ -125,6 +130,19 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form)); 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) { private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data }; let send = { op: UserOperation[op], data: data };
console.log(send); console.log(send);
@ -138,7 +156,6 @@ export class WebSocketService {
throw "Not logged in"; throw "Not logged in";
} }
} }
} }
window.onbeforeunload = (() => { window.onbeforeunload = (() => {