## 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
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)
+drop table site;
drop table community_user_ban;;
drop table community_moderator;
drop table community_follower;
);
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
+);
drop view community_moderator_view;
drop view community_follower_view;
drop view community_user_ban_view;
+drop view site_view;
(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;
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
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};
pub updated: Option<chrono::NaiveDateTime>
}
+impl Crud<CommunityForm> for Community {
+ fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
+ use schema::community::dsl::*;
+ community.find(community_id)
+ .first::<Self>(conn)
+ }
+
+ fn delete(conn: &PgConnection, community_id: i32) -> Result<usize, Error> {
+ use schema::community::dsl::*;
+ diesel::delete(community.find(community_id))
+ .execute(conn)
+ }
+
+ fn create(conn: &PgConnection, new_community: &CommunityForm) -> Result<Self, Error> {
+ use schema::community::dsl::*;
+ insert_into(community)
+ .values(new_community)
+ .get_result::<Self>(conn)
+ }
+
+ fn update(conn: &PgConnection, community_id: i32, new_community: &CommunityForm) -> Result<Self, Error> {
+ use schema::community::dsl::*;
+ diesel::update(community.find(community_id))
+ .set(new_community)
+ .get_result::<Self>(conn)
+ }
+}
+
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_moderator"]
pub user_id: i32,
}
+impl Joinable<CommunityModeratorForm> for CommunityModerator {
+ fn join(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result<Self, Error> {
+ use schema::community_moderator::dsl::*;
+ insert_into(community_moderator)
+ .values(community_user_form)
+ .get_result::<Self>(conn)
+ }
+
+ fn leave(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result<usize, Error> {
+ use schema::community_moderator::dsl::*;
+ diesel::delete(community_moderator
+ .filter(community_id.eq(community_user_form.community_id))
+ .filter(user_id.eq(community_user_form.user_id)))
+ .execute(conn)
+ }
+}
+
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_user_ban"]
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::*;
+ insert_into(community_user_ban)
+ .values(community_user_ban_form)
+ .get_result::<Self>(conn)
+ }
+
+ fn unban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<usize, Error> {
+ use schema::community_user_ban::dsl::*;
+ diesel::delete(community_user_ban
+ .filter(community_id.eq(community_user_ban_form.community_id))
+ .filter(user_id.eq(community_user_ban_form.user_id)))
+ .execute(conn)
+ }
+}
+
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_follower"]
pub user_id: i32,
}
-
-impl Crud<CommunityForm> for Community {
- fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
- use schema::community::dsl::*;
- community.find(community_id)
- .first::<Self>(conn)
- }
-
- fn delete(conn: &PgConnection, community_id: i32) -> Result<usize, Error> {
- use schema::community::dsl::*;
- diesel::delete(community.find(community_id))
- .execute(conn)
- }
-
- fn create(conn: &PgConnection, new_community: &CommunityForm) -> Result<Self, Error> {
- use schema::community::dsl::*;
- insert_into(community)
- .values(new_community)
- .get_result::<Self>(conn)
- }
-
- fn update(conn: &PgConnection, community_id: i32, new_community: &CommunityForm) -> Result<Self, Error> {
- use schema::community::dsl::*;
- diesel::update(community.find(community_id))
- .set(new_community)
- .get_result::<Self>(conn)
- }
-}
-
impl Followable<CommunityFollowerForm> for CommunityFollower {
fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> {
use schema::community_follower::dsl::*;
}
}
-impl Joinable<CommunityModeratorForm> for CommunityModerator {
- fn join(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result<Self, Error> {
- use schema::community_moderator::dsl::*;
- insert_into(community_moderator)
- .values(community_user_form)
- .get_result::<Self>(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 leave(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result<usize, Error> {
- use schema::community_moderator::dsl::*;
- diesel::delete(community_moderator
- .filter(community_id.eq(community_user_form.community_id))
- .filter(user_id.eq(community_user_form.user_id)))
+ fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
+ use schema::site::dsl::*;
+ diesel::delete(site.find(site_id))
.execute(conn)
}
-}
-impl Bannable<CommunityUserBanForm> for CommunityUserBan {
- fn ban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<Self, Error> {
- use schema::community_user_ban::dsl::*;
- insert_into(community_user_ban)
- .values(community_user_ban_form)
- .get_result::<Self>(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 unban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<usize, Error> {
- use schema::community_user_ban::dsl::*;
- diesel::delete(community_user_ban
- .filter(community_id.eq(community_user_ban_form.community_id))
- .filter(user_id.eq(community_user_ban_form.user_id)))
- .execute(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)
}
}
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
}
}
+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 {
.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)
+ }
+}
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
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();
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>
}
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>
}
.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)
+
}
}
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
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
};
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);
}
}
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,
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,
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)
+ }
}
email: None,
icon: None,
published: naive_now(),
- admin: None,
- banned: None,
+ admin: false,
+ banned: false,
updated: None
};
}
}
+table! {
+ site (id) {
+ id -> Int4,
+ name -> Varchar,
+ description -> Nullable<Text>,
+ creator_id -> Int4,
+ published -> Timestamp,
+ updated -> Nullable<Timestamp>,
+ }
+}
+
table! {
user_ (id) {
id -> Int4,
password_encrypted -> Text,
email -> Nullable<Text>,
icon -> Nullable<Bytea>,
- admin -> Nullable<Bool>,
- banned -> Nullable<Bool>,
+ admin -> Bool,
+ banned -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
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!(
mod_remove_post,
post,
post_like,
+ site,
user_,
user_ban,
);
#[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)]
username: String,
email: Option<String>,
password: String,
- password_verify: String
+ password_verify: String,
+ admin: bool,
}
#[derive(Serialize, Deserialize)]
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 {
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)
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(),
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 {
}
}
+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
+
+ }
+}
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",
}
}
handleCommentSubmit(i: CommentForm, event: any) {
+ event.preventDefault();
if (i.props.edit) {
WebSocketService.Instance.editComment(i.state.commentForm);
} else {
}
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) {
}
handleModRemoveSubmit(i: CommentNode) {
+ event.preventDefault();
let form: CommentFormI = {
content: i.props.node.comment.content,
edit_id: i.props.node.comment.id,
}
handleModBanSubmit(i: CommentNode) {
+ event.preventDefault();
let form: BanFromCommunityForm = {
user_id: i.props.node.comment.creator_id,
community_id: i.props.node.comment.community_id,
interface CommentNodesProps {
nodes: Array<CommentNodeI>;
- moderators: Array<CommunityUser>;
+ moderators?: Array<CommunityUser>;
noIndent?: boolean;
viewOnly?: boolean;
locked?: boolean;
<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>
<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>
--- /dev/null
+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>
+ );
+ }
+}
+
registerForm: {
username: undefined,
password: undefined,
- password_verify: undefined
+ password_verify: undefined,
+ admin: false,
},
loginLoading: false,
registerLoading: false
}
handleRegisterSubmit(i: Login, event: any) {
+ event.preventDefault();
i.state.registerLoading = true;
i.setState(i.state);
event.preventDefault();
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;
}
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
}
() => console.log('complete')
);
- if (UserService.Instance.loggedIn) {
+ WebSocketService.Instance.getSite();
+
+ if (UserService.Instance.user) {
WebSocketService.Instance.getFollowedCommunities();
}
<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">
<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>
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>
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>
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);
+ }
}
}
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(
);
let modlogForm: GetModlogForm = {
-
+ community_id: this.state.communityId
};
WebSocketService.Instance.getModlog(modlogForm);
}
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");
-
- 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);
+ 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");
+
+ 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>
{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">
} 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);
}
}
}
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
-import { repoUrl } from '../utils';
import { UserService } from '../services';
import { version } from '../version';
export class Navbar extends Component<any, NavbarState> {
emptyState: NavbarState = {
- isLoggedIn: UserService.Instance.loggedIn,
+ isLoggedIn: UserService.Instance.user !== undefined,
expanded: false,
expandUserDropdown: false
}
</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>
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
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
</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>
<div class="col-sm-10">
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
)}
</select>
</div>
- </div>
+ </div>
+ }
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2">
}
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) {
}
handleModRemoveSubmit(i: PostListing) {
+ event.preventDefault();
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
sortType: SortType.Hot,
type_: this.props.communityId
? ListingType.Community
- : UserService.Instance.loggedIn
+ : UserService.Instance.user
? ListingType.Subscribed
: ListingType.All,
loading: true
{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>
}
<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>
{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">
--- /dev/null
+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('/');
+ }
+ }
+}
</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
<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>
);
}
}
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 {
}
handleModRemoveSubmit(i: Sidebar) {
-
+ event.preventDefault();
let deleteForm: CommunityFormI = {
name: i.props.community.name,
title: i.props.community.title,
--- /dev/null
+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();
+ }
+}
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';
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';
<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>
);
}
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 {
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;
email?: string;
password: string;
password_verify: string;
+ admin: boolean;
}
export interface LoginResponse {
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>;
+}
this.sub.next(undefined);
}
- public get loggedIn(): boolean {
- return this.user !== undefined;
- }
-
public get auth(): string {
return Cookies.get("jwt");
}
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';
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();
- console.log(`Connected to ${wsUri}`);
+ console.log(`Connected to ${wsUri}`);
}
public static get Instance(){
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);
throw "Not logged in";
}
}
-
}
window.onbeforeunload = (() => {