diff --git a/Dockerfile b/Dockerfile index 9a7caccd..e2803851 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,39 @@ FROM node:10-jessie as node #If encounter Invalid cross-device error -run on host 'echo N | sudo tee /sys/module/overlay/parameters/metacopy' COPY ui /app/ui -RUN cd /app/ui && yarn && yarn build +WORKDIR /app/ui +RUN yarn +RUN yarn build FROM rust:1.33 as rust -COPY server /app/server + +# create a new empty shell project +WORKDIR /app +RUN USER=root cargo new server +WORKDIR /app/server + +# copy over your manifests +COPY server/Cargo.toml server/Cargo.lock ./ + +# this build step will cache your dependencies +RUN mkdir -p ./src/bin \ + && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs +RUN cargo build --release --bin lemmy +RUN ls ./target/release/.fingerprint/ +RUN rm -r ./target/release/.fingerprint/server-* + +# copy your source tree +# RUN rm -rf ./src/ +COPY server/src ./src/ +COPY server/migrations ./migrations/ + +# build for release +RUN cargo build --frozen --release --bin lemmy +RUN mv /app/server/target/release/lemmy /app/lemmy + +# The output image +# FROM debian:stable-slim +# RUN apt-get -y update && apt-get install -y postgresql-client +# COPY --from=rust /app/server/target/release/lemmy /app/lemmy COPY --from=node /app/ui/dist /app/dist -RUN cd /app/server && cargo build --release -RUN mv /app/server/target/release/lemmy /app/ -WORKDIR /app/ EXPOSE 8536 diff --git a/docker-compose.yml b/docker-compose.yml index 940cd0f6..03c72881 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,9 +10,9 @@ services: POSTGRES_DB: rrr healthcheck: test: ["CMD-SHELL", "pg_isready -U rrr"] - interval: 30s - timeout: 30s - retries: 3 + interval: 5s + timeout: 5s + retries: 20 lemmy: build: context: . @@ -22,6 +22,7 @@ services: environment: LEMMY_FRONT_END_DIR: /app/dist DATABASE_URL: postgres://rrr:rrr@db:5432/rrr + restart: always depends_on: db: condition: service_healthy diff --git a/server/Cargo.lock b/server/Cargo.lock index ce4e175a..36d2a901 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1359,6 +1359,7 @@ dependencies = [ "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "jsonwebtoken 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/server/Cargo.toml b/server/Cargo.toml index fd3d5773..93bd6acb 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -24,4 +24,5 @@ rand = "0.6.5" strum = "0.14.0" strum_macros = "0.14.0" jsonwebtoken = "*" -regex = "1" +regex = "*" +lazy_static = "*" diff --git a/server/src/actions/community_view.rs b/server/src/actions/community_view.rs index 185484bf..cb89b226 100644 --- a/server/src/actions/community_view.rs +++ b/server/src/actions/community_view.rs @@ -2,6 +2,7 @@ extern crate diesel; use diesel::*; use diesel::result::Error; use serde::{Deserialize, Serialize}; +use {SortType}; table! { community_view (id) { @@ -83,17 +84,27 @@ impl CommunityView { query.first::(conn) } - pub fn list_all(conn: &PgConnection, from_user_id: Option) -> Result, Error> { + pub fn list(conn: &PgConnection, from_user_id: Option, sort: SortType, limit: Option) -> Result, Error> { use actions::community_view::community_view::dsl::*; let mut query = community_view.into_boxed(); + + // The view lets you pass a null user_id, if you're not logged in - if let Some(from_user_id) = from_user_id { - query = query.filter(user_id.eq(from_user_id)) - .order_by((subscribed.desc(), number_of_subscribers.desc())); - } else { - query = query.filter(user_id.is_null()) - .order_by(number_of_subscribers.desc()); + + match sort { + SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()), + SortType::TopAll => { + match from_user_id { + Some(from_user_id) => query = query.filter(user_id.eq(from_user_id)).order_by((subscribed.desc(), number_of_subscribers.desc())), + None => query = query.order_by(number_of_subscribers.desc()).filter(user_id.is_null()) + } + } + _ => () + }; + + if let Some(limit) = limit { + query = query.limit(limit); }; query.load::(conn) diff --git a/server/src/lib.rs b/server/src/lib.rs index 9cdbd33e..814363b4 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -12,7 +12,7 @@ pub extern crate jsonwebtoken; pub extern crate bcrypt; pub extern crate regex; #[macro_use] pub extern crate strum_macros; - +#[macro_use] pub extern crate lazy_static; pub mod schema; pub mod apub; pub mod actions; @@ -89,21 +89,42 @@ pub fn naive_now() -> NaiveDateTime { } pub fn is_email_regex(test: &str) -> bool { - let re = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); - re.is_match(test) + EMAIL_REGEX.is_match(test) +} + +pub fn remove_slurs(test: &str) -> String { + SLUR_REGEX.replace_all(test, "*removed*").to_string() +} + +pub fn has_slurs(test: &str) -> bool { + SLUR_REGEX.is_match(test) } #[cfg(test)] mod tests { - use {Settings, is_email_regex}; + use {Settings, is_email_regex, remove_slurs, has_slurs}; #[test] fn test_api() { assert_eq!(Settings::get().api_endpoint(), "http://0.0.0.0/api/v1"); } - #[test] - fn test_email() { + #[test] fn test_email() { assert!(is_email_regex("gush@gmail.com")); assert!(!is_email_regex("nada_neutho")); } + + #[test] fn test_slur_filter() { + let test = "coons test dindu ladyboy tranny. This is a bunch of other safe text.".to_string(); + let slur_free = "No slurs here"; + assert_eq!(remove_slurs(&test), "*removed* test *removed* *removed* *removed*. This is a bunch of other safe text.".to_string()); + assert!(has_slurs(&test)); + assert!(!has_slurs(slur_free)); + } } + + +lazy_static! { + static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); + static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bnig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?))").unwrap(); +} + diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index 92542d0a..137761ab 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -10,7 +10,7 @@ use serde_json::{Value}; use bcrypt::{verify}; use std::str::FromStr; -use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now, SortType}; +use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now, SortType, has_slurs, remove_slurs}; use actions::community::*; use actions::user::*; use actions::post::*; @@ -111,6 +111,8 @@ pub struct CommunityResponse { #[derive(Serialize, Deserialize)] pub struct ListCommunities { + sort: String, + limit: Option, auth: Option } @@ -391,25 +393,25 @@ impl Handler for ChatServer { let json: Value = serde_json::from_str(&msg.msg) .expect("Couldn't parse message"); - let data: &Value = &json["data"]; + let data = &json["data"].to_string(); let op = &json["op"].as_str().unwrap(); let user_operation: UserOperation = UserOperation::from_str(&op).unwrap(); let res: String = match user_operation { UserOperation::Login => { - let login: Login = serde_json::from_str(&data.to_string()).unwrap(); + let login: Login = serde_json::from_str(data).unwrap(); login.perform(self, msg.id) }, UserOperation::Register => { - let register: Register = serde_json::from_str(&data.to_string()).unwrap(); + let register: Register = serde_json::from_str(data).unwrap(); register.perform(self, msg.id) }, UserOperation::CreateCommunity => { - let create_community: CreateCommunity = serde_json::from_str(&data.to_string()).unwrap(); + let create_community: CreateCommunity = serde_json::from_str(data).unwrap(); create_community.perform(self, msg.id) }, UserOperation::ListCommunities => { - let list_communities: ListCommunities = serde_json::from_str(&data.to_string()).unwrap(); + let list_communities: ListCommunities = serde_json::from_str(data).unwrap(); list_communities.perform(self, msg.id) }, UserOperation::ListCategories => { @@ -417,55 +419,55 @@ impl Handler for ChatServer { list_categories.perform(self, msg.id) }, UserOperation::CreatePost => { - let create_post: CreatePost = serde_json::from_str(&data.to_string()).unwrap(); + let create_post: CreatePost = serde_json::from_str(data).unwrap(); create_post.perform(self, msg.id) }, UserOperation::GetPost => { - let get_post: GetPost = serde_json::from_str(&data.to_string()).unwrap(); + let get_post: GetPost = serde_json::from_str(data).unwrap(); get_post.perform(self, msg.id) }, UserOperation::GetCommunity => { - let get_community: GetCommunity = serde_json::from_str(&data.to_string()).unwrap(); + let get_community: GetCommunity = serde_json::from_str(data).unwrap(); get_community.perform(self, msg.id) }, UserOperation::CreateComment => { - let create_comment: CreateComment = serde_json::from_str(&data.to_string()).unwrap(); + let create_comment: CreateComment = serde_json::from_str(data).unwrap(); create_comment.perform(self, msg.id) }, UserOperation::EditComment => { - let edit_comment: EditComment = serde_json::from_str(&data.to_string()).unwrap(); + let edit_comment: EditComment = serde_json::from_str(data).unwrap(); edit_comment.perform(self, msg.id) }, UserOperation::CreateCommentLike => { - let create_comment_like: CreateCommentLike = serde_json::from_str(&data.to_string()).unwrap(); + let create_comment_like: CreateCommentLike = serde_json::from_str(data).unwrap(); create_comment_like.perform(self, msg.id) }, UserOperation::GetPosts => { - let get_posts: GetPosts = serde_json::from_str(&data.to_string()).unwrap(); + let get_posts: GetPosts = serde_json::from_str(data).unwrap(); get_posts.perform(self, msg.id) }, UserOperation::CreatePostLike => { - let create_post_like: CreatePostLike = serde_json::from_str(&data.to_string()).unwrap(); + let create_post_like: CreatePostLike = serde_json::from_str(data).unwrap(); create_post_like.perform(self, msg.id) }, UserOperation::EditPost => { - let edit_post: EditPost = serde_json::from_str(&data.to_string()).unwrap(); + let edit_post: EditPost = serde_json::from_str(data).unwrap(); edit_post.perform(self, msg.id) }, UserOperation::EditCommunity => { - let edit_community: EditCommunity = serde_json::from_str(&data.to_string()).unwrap(); + let edit_community: EditCommunity = serde_json::from_str(data).unwrap(); edit_community.perform(self, msg.id) }, UserOperation::FollowCommunity => { - let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap(); + let follow_community: FollowCommunity = serde_json::from_str(data).unwrap(); follow_community.perform(self, msg.id) }, UserOperation::GetFollowedCommunities => { - let followed_communities: GetFollowedCommunities = serde_json::from_str(&data.to_string()).unwrap(); + let followed_communities: GetFollowedCommunities = serde_json::from_str(data).unwrap(); followed_communities.perform(self, msg.id) }, UserOperation::GetUserDetails => { - let get_user_details: GetUserDetails = serde_json::from_str(&data.to_string()).unwrap(); + let get_user_details: GetUserDetails = serde_json::from_str(data).unwrap(); get_user_details.perform(self, msg.id) }, // _ => { @@ -541,6 +543,10 @@ impl Perform for Register { return self.error("Passwords do not match."); } + if has_slurs(&self.username) { + return self.error("No slurs"); + } + // Register the new user let user_form = UserForm { name: self.username.to_owned(), @@ -587,6 +593,12 @@ impl Perform for CreateCommunity { } }; + if has_slurs(&self.name) || + has_slurs(&self.title) || + (self.description.is_some() && has_slurs(&self.description.to_owned().unwrap())) { + return self.error("No slurs"); + } + let user_id = claims.id; // When you create a community, make sure the user becomes a moderator and a follower @@ -665,7 +677,9 @@ impl Perform for ListCommunities { None => None }; - let communities: Vec = CommunityView::list_all(&conn, user_id).unwrap(); + let sort = SortType::from_str(&self.sort).expect("listing sort"); + + let communities: Vec = CommunityView::list(&conn, user_id, sort, self.limit).unwrap(); // Return the jwt serde_json::to_string( @@ -716,6 +730,11 @@ impl Perform for CreatePost { } }; + if has_slurs(&self.name) || + (self.body.is_some() && has_slurs(&self.body.to_owned().unwrap())) { + return self.error("No slurs"); + } + let user_id = claims.id; let post_form = PostForm { @@ -894,8 +913,10 @@ impl Perform for CreateComment { let user_id = claims.id; + let content_slurs_removed = remove_slurs(&self.content.to_owned()); + let comment_form = CommentForm { - content: self.content.to_owned(), + content: content_slurs_removed, parent_id: self.parent_id.to_owned(), post_id: self.post_id, creator_id: user_id, @@ -976,8 +997,10 @@ impl Perform for EditComment { return self.error("Incorrect creator."); } + let content_slurs_removed = remove_slurs(&self.content.to_owned()); + let comment_form = CommentForm { - content: self.content.to_owned(), + content: content_slurs_removed, parent_id: self.parent_id, post_id: self.post_id, creator_id: user_id, @@ -1197,6 +1220,11 @@ impl Perform for EditPost { fn perform(&self, chat: &mut ChatServer, addr: usize) -> String { + if has_slurs(&self.name) || + (self.body.is_some() && has_slurs(&self.body.to_owned().unwrap())) { + return self.error("No slurs"); + } + let conn = establish_connection(); let claims = match Claims::decode(&self.auth) { @@ -1264,6 +1292,10 @@ impl Perform for EditCommunity { fn perform(&self, chat: &mut ChatServer, addr: usize) -> String { + if has_slurs(&self.name) || has_slurs(&self.title) { + return self.error("No slurs"); + } + let conn = establish_connection(); let claims = match Claims::decode(&self.auth) { diff --git a/ui/fuse.js b/ui/fuse.js index fe2c7664..0fdf9a42 100644 --- a/ui/fuse.js +++ b/ui/fuse.js @@ -11,7 +11,7 @@ const transformInferno = require('ts-transform-inferno').default; const transformClasscat = require('ts-transform-classcat').default; let fuse, app; let isProduction = false; -var setVersion = require('./set_version.js').setVersion; +// var setVersion = require('./set_version.js').setVersion; Sparky.task('config', _ => { fuse = new FuseBox({ @@ -42,7 +42,7 @@ Sparky.task('config', _ => { }); app = fuse.bundle('app').instructions('>index.tsx'); }); -Sparky.task('version', _ => setVersion()); +// Sparky.task('version', _ => setVersion()); Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); Sparky.task('env', _ => (isProduction = true)); Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/')); diff --git a/ui/set_version.js b/ui/set_version.js old mode 100644 new mode 100755 index bfd640c2..21893085 --- a/ui/set_version.js +++ b/ui/set_version.js @@ -7,3 +7,5 @@ exports.setVersion = function() { let line = `export let version: string = "${revision}";`; fs.writeFileSync("./src/version.ts", line); } + +this.setVersion() diff --git a/ui/src/components/communities.tsx b/ui/src/components/communities.tsx index cf42238e..4d2512cc 100644 --- a/ui/src/components/communities.tsx +++ b/ui/src/components/communities.tsx @@ -2,7 +2,7 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; -import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm } from '../interfaces'; +import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces'; import { WebSocketService } from '../services'; import { msgOp } from '../utils'; @@ -30,7 +30,12 @@ export class Communities extends Component { (err) => console.error(err), () => console.log('complete') ); - WebSocketService.Instance.listCommunities(); + + let listCommunitiesForm: ListCommunitiesForm = { + sort: SortType[SortType.TopAll] + } + + WebSocketService.Instance.listCommunities(listCommunitiesForm); } @@ -45,7 +50,7 @@ export class Communities extends Component { render() { return ( -
+
{this.state.loading ?

:
diff --git a/ui/src/components/community-form.tsx b/ui/src/components/community-form.tsx index b5b222c6..056c29dc 100644 --- a/ui/src/components/community-form.tsx +++ b/ui/src/components/community-form.tsx @@ -155,6 +155,7 @@ export class CommunityForm extends Component {
- +
- +
diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 477eec65..55066d00 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -2,13 +2,14 @@ import { Component } from 'inferno'; import { Link } from 'inferno-router'; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; -import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse } from '../interfaces'; +import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { PostListings } from './post-listings'; -import { msgOp } from '../utils'; +import { msgOp, repoUrl } from '../utils'; interface State { subscribedCommunities: Array; + trendingCommunities: Array; loading: boolean; } @@ -17,6 +18,7 @@ export class Main extends Component { private subscription: Subscription; private emptyState: State = { subscribedCommunities: [], + trendingCommunities: [], loading: true } @@ -36,6 +38,13 @@ export class Main extends Component { if (UserService.Instance.loggedIn) { WebSocketService.Instance.getFollowedCommunities(); } + + let listCommunitiesForm: ListCommunitiesForm = { + sort: SortType[SortType.New], + limit: 8 + } + + WebSocketService.Instance.listCommunities(listCommunitiesForm); } componentWillUnmount() { @@ -46,26 +55,26 @@ export class Main extends Component { return (
-
+
-
-

A Landing message

- {UserService.Instance.loggedIn && +
+ {this.state.loading ? +

: +
+ {this.trendingCommunities()} + {UserService.Instance.loggedIn ?
- {this.state.loading ? -

: -
-
-

Subscribed forums

-
    - {this.state.subscribedCommunities.map(community => -
  • {community.community_name}
  • - )} -
-
- } -
+

Subscribed forums

+
    + {this.state.subscribedCommunities.map(community => +
  • {community.community_name}
  • + )} +
+
: + this.landing() + } +
}
@@ -73,6 +82,34 @@ export class Main extends Component { ) } + trendingCommunities() { + return ( +
+

Trending forums

+
    + {this.state.trendingCommunities.map(community => +
  • {community.name}
  • + )} +
+
+ ) + } + + landing() { + return ( +
+

Welcome to + + LemmyBeta +

+

Lemmy is a link aggregator / reddit alternative, intended to work in the fediverse.

+

Its self-hostable, has live-updating comment threads, and is tiny (~80kB). Federation into the ActivityPub network is on the roadmap.

+

This is a very early beta version, and a lot of features are currently broken or missing.

+

Suggest new features or report bugs here.

+

Made with Rust, Actix, Inferno, Typescript.

+
+ ) + } parseMessage(msg: any) { console.log(msg); @@ -85,6 +122,11 @@ export class Main extends Component { this.state.subscribedCommunities = res.communities; this.state.loading = false; this.setState(this.state); + } else if (op == UserOperation.ListCommunities) { + let res: ListCommunitiesResponse = msg; + this.state.trendingCommunities = res.communities; + this.state.loading = false; + this.setState(this.state); } } } diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index ca0c5a2a..5c51b699 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -25,7 +25,7 @@ export class Navbar extends Component { // Subscribe to user changes UserService.Instance.sub.subscribe(user => { - let loggedIn: boolean = user !== null; + let loggedIn: boolean = user !== undefined; this.setState({isLoggedIn: loggedIn}); }); } @@ -40,7 +40,7 @@ export class Navbar extends Component { // TODO toggle css collapse navbar() { return ( -
: - Login + Login / Sign up }
diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 9845a1b1..67a3f42e 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -1,7 +1,7 @@ import { Component, linkEvent } from 'inferno'; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; -import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse } from '../interfaces'; +import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType } from '../interfaces'; import { WebSocketService } from '../services'; import { msgOp } from '../utils'; import * as autosize from 'autosize'; @@ -56,7 +56,11 @@ export class PostForm extends Component { () => console.log('complete') ); - WebSocketService.Instance.listCommunities(); + let listCommunitiesForm: ListCommunitiesForm = { + sort: SortType[SortType.TopAll] + } + + WebSocketService.Instance.listCommunities(listCommunitiesForm); } componentDidMount() { @@ -151,7 +155,9 @@ export class PostForm extends Component { parseMessage(msg: any) { let op: UserOperation = msgOp(msg); if (msg.error) { + alert(msg.error); this.state.loading = false; + this.setState(this.state); return; } else if (op == UserOperation.ListCommunities) { let res: ListCommunitiesResponse = msg; diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index f36893f6..5ca3f770 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -101,17 +101,17 @@ export class Post extends Component { sortRadios() { return (
-