Merge remote-tracking branch 'LemmyNet/master'

pull/959/head
derek 4 years ago
commit d71897620c

3
.travis.yml vendored

@ -24,10 +24,11 @@ script:
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf - cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
- cargo install diesel_cli --no-default-features --features postgres --force - cargo install diesel_cli --no-default-features --features postgres --force
- diesel migration run - diesel migration run
- cargo test - cargo test --workspace
env: env:
global: global:
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy - DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
- LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
- RUST_TEST_THREADS=1 - RUST_TEST_THREADS=1
addons: addons:

2
ansible/VERSION vendored

@ -1 +1 @@
v0.7.13 v0.7.19

@ -18,11 +18,12 @@ RUN sudo chown -R rust:rust .
RUN USER=root cargo new server RUN USER=root cargo new server
WORKDIR /app/server WORKDIR /app/server
COPY server/Cargo.toml server/Cargo.lock ./ COPY server/Cargo.toml server/Cargo.lock ./
RUN sudo chown -R rust:rust . COPY server/lemmy_db ./lemmy_db
COPY server/lemmy_utils ./lemmy_utils
RUN mkdir -p ./src/bin \ RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN cargo build RUN cargo build
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server* RUN find target/debug -type f -name "$(echo "lemmy_server" | tr '-' '_')*" -exec touch -t 200001010000 {} +
COPY server/src ./src/ COPY server/src ./src/
COPY server/migrations ./migrations/ COPY server/migrations ./migrations/

@ -10,14 +10,15 @@ WORKDIR /app
RUN sudo chown -R rust:rust . RUN sudo chown -R rust:rust .
RUN USER=root cargo new server RUN USER=root cargo new server
WORKDIR /app/server WORKDIR /app/server
COPY --chown=rust:rust server/Cargo.toml server/Cargo.lock ./ COPY server/Cargo.toml server/Cargo.lock ./
#RUN sudo chown -R rust:rust . COPY server/lemmy_db ./lemmy_db
COPY server/lemmy_utils ./lemmy_utils
RUN mkdir -p ./src/bin \ RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN cargo build --release RUN cargo build --release
RUN rm -f ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/deps/lemmy_server* RUN find target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR -type f -name "$(echo "lemmy_server" | tr '-' '_')*" -exec touch -t 200001010000 {} +
COPY --chown=rust:rust server/src ./src/ COPY server/src ./src/
COPY --chown=rust:rust server/migrations ./migrations/ COPY server/migrations ./migrations/
# build for release # build for release
# workaround for https://github.com/rust-lang/rust/issues/62896 # workaround for https://github.com/rust-lang/rust/issues/62896

@ -12,7 +12,7 @@ services:
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.7.13 image: dessalines/lemmy:v0.7.19
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always

@ -5,6 +5,8 @@ The configuration is based on the file
This file also contains documentation for all the available options. To override the defaults, you This file also contains documentation for all the available options. To override the defaults, you
can copy the options you want to change into your local `config.hjson` file. can copy the options you want to change into your local `config.hjson` file.
To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`.
Additionally, you can override any config files with environment variables. These have the same Additionally, you can override any config files with environment variables. These have the same
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
`database.password` with `LEMMY_DATABASE__POOL_SIZE=10`. `database.password` with `LEMMY_DATABASE__POOL_SIZE=10`.

@ -7,9 +7,7 @@ following commands in the `server` subfolder:
```bash ```bash
psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy ./test.sh
diesel migration run
RUST_TEST_THREADS=1 cargo test
``` ```
### Federation ### Federation

@ -1149,6 +1149,7 @@ Post listing types are `All, Subscribed, Community`
page: Option<i64>, page: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
community_id: Option<i32>, community_id: Option<i32>,
community_name: Option<String>,
auth: Option<String> auth: Option<String>
} }
} }

2
install.sh vendored

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/bash
set -e set -e
# Set the database variable to the default first. # Set the database variable to the default first.

53
server/Cargo.lock generated vendored

@ -1399,12 +1399,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "htmlescape"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.1" version = "0.2.1"
@ -1428,9 +1422,9 @@ dependencies = [
[[package]] [[package]]
name = "http-signature-normalization-actix" name = "http-signature-normalization-actix"
version = "0.4.0-alpha.0" version = "0.4.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09afff6987c7edbed101d1cddd2185786fb0af0dd9c06b654aca73a0a763680f" checksum = "131fc982391a6b37847888b568cbe0e9cd302f1b0015f4f6f4a50234bebd049c"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-web", "actix-web",
@ -1572,6 +1566,21 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lemmy_db"
version = "0.1.0"
dependencies = [
"bcrypt",
"chrono",
"diesel",
"log",
"serde 1.0.114",
"serde_json",
"sha2",
"strum",
"strum_macros",
]
[[package]] [[package]]
name = "lemmy_server" name = "lemmy_server"
version = "0.0.1" version = "0.0.1"
@ -1589,27 +1598,23 @@ dependencies = [
"base64 0.12.3", "base64 0.12.3",
"bcrypt", "bcrypt",
"chrono", "chrono",
"comrak",
"config",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"dotenv", "dotenv",
"env_logger", "env_logger",
"failure", "failure",
"futures", "futures",
"htmlescape",
"http", "http",
"http-signature-normalization-actix", "http-signature-normalization-actix",
"itertools", "itertools",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
"lettre", "lemmy_db",
"lettre_email", "lemmy_utils",
"log", "log",
"openssl", "openssl",
"percent-encoding", "percent-encoding",
"rand 0.7.3", "rand 0.7.3",
"regex",
"rss", "rss",
"serde 1.0.114", "serde 1.0.114",
"serde_json", "serde_json",
@ -1621,6 +1626,26 @@ dependencies = [
"uuid 0.8.1", "uuid 0.8.1",
] ]
[[package]]
name = "lemmy_utils"
version = "0.1.0"
dependencies = [
"chrono",
"comrak",
"config",
"itertools",
"lazy_static",
"lettre",
"lettre_email",
"log",
"openssl",
"rand 0.7.3",
"regex",
"serde 1.0.114",
"serde_json",
"url",
]
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.9.3" version = "0.9.3"

19
server/Cargo.toml vendored

@ -1,14 +1,21 @@
[package] [package]
name = "lemmy_server" name = "lemmy_server"
version = "0.0.1" version = "0.0.1"
authors = ["Dessalines <tyhou13@gmx.com>"]
edition = "2018" edition = "2018"
[profile.release] [profile.release]
lto = true lto = true
[workspace]
members = [
"lemmy_utils",
"lemmy_db"
]
[dependencies] [dependencies]
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] } lemmy_utils = { path = "./lemmy_utils" }
lemmy_db = { path = "./lemmy_db" }
diesel = "1.4.4"
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
dotenv = "0.15.0" dotenv = "0.15.0"
activitystreams = "0.6.2" activitystreams = "0.6.2"
@ -31,19 +38,13 @@ rand = "0.7.3"
strum = "0.18.0" strum = "0.18.0"
strum_macros = "0.18.0" strum_macros = "0.18.0"
jsonwebtoken = "7.0.1" jsonwebtoken = "7.0.1"
regex = "1.3.5"
lazy_static = "1.3.0" lazy_static = "1.3.0"
lettre = "0.9.3"
lettre_email = "0.9.4"
rss = "1.9.0" rss = "1.9.0"
htmlescape = "0.3.1"
url = { version = "2.1.1", features = ["serde"] } url = { version = "2.1.1", features = ["serde"] }
config = {version = "0.10.1", default-features = false, features = ["hjson"] }
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
comrak = "0.7"
openssl = "0.10" openssl = "0.10"
http = "0.2.1" http = "0.2.1"
http-signature-normalization-actix = { version = "0.4.0-alpha.0", default-features = false, features = ["sha-2"] } http-signature-normalization-actix = { version = "0.4.0-alpha.2", default-features = false, features = ["sha-2"] }
base64 = "0.12.1" base64 = "0.12.1"
tokio = "0.2.21" tokio = "0.2.21"
futures = "0.3.5" futures = "0.3.5"

3
server/db-init.sh vendored

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/bash
set -e
# Default configurations # Default configurations
username=lemmy username=lemmy

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

@ -0,0 +1,15 @@
[package]
name = "lemmy_db"
version = "0.1.0"
edition = "2018"
[dependencies]
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
chrono = { version = "0.4.7", features = ["serde"] }
serde = { version = "1.0.105", features = ["derive"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]}
strum = "0.18.0"
strum_macros = "0.18.0"
log = "0.4.0"
sha2 = "0.9"
bcrypt = "0.8.0"

@ -1,9 +1,12 @@
use crate::{blocking, db::Crud, schema::activity, DbPool, LemmyError}; use crate::{schema::activity, Crud};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fmt::Debug; use std::{
fmt::Debug,
io::{Error as IoError, ErrorKind},
};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "activity"] #[table_name = "activity"]
@ -55,46 +58,43 @@ impl Crud<ActivityForm> for Activity {
} }
} }
pub async fn insert_activity<T>( pub fn do_insert_activity<T>(
user_id: i32,
data: T,
local: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + Debug + Send + 'static,
{
blocking(pool, move |conn| {
do_insert_activity(conn, user_id, &data, local)
})
.await??;
Ok(())
}
fn do_insert_activity<T>(
conn: &PgConnection, conn: &PgConnection,
user_id: i32, user_id: i32,
data: &T, data: &T,
local: bool, local: bool,
) -> Result<(), LemmyError> ) -> Result<Activity, IoError>
where where
T: Serialize + Debug, T: Serialize + Debug,
{ {
debug!("inserting activity for user {}, data {:?}", user_id, &data);
let activity_form = ActivityForm { let activity_form = ActivityForm {
user_id, user_id,
data: serde_json::to_value(&data)?, data: serde_json::to_value(&data)?,
local, local,
updated: None, updated: None,
}; };
debug!("inserting activity for user {}, data {:?}", user_id, data); let result = Activity::create(&conn, &activity_form);
Activity::create(&conn, &activity_form)?; match result {
Ok(()) Ok(s) => Ok(s),
Err(e) => Err(IoError::new(
ErrorKind::Other,
format!("Failed to insert activity into database: {}", e),
)),
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use crate::{
use crate::db::{establish_unpooled_connection, Crud, ListingType, SortType}; activity::{Activity, ActivityForm},
tests::establish_unpooled_connection,
user::{UserForm, User_},
Crud,
ListingType,
SortType,
};
use serde_json::Value;
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,6 +1,6 @@
use crate::{ use crate::{
db::Crud,
schema::{category, category::dsl::*}, schema::{category, category::dsl::*},
Crud,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -52,8 +52,7 @@ impl Category {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use crate::{category::Category, tests::establish_unpooled_connection};
use crate::db::establish_unpooled_connection;
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,9 +1,5 @@
use super::{post::Post, *}; use super::{post::Post, *};
use crate::{ use crate::schema::{comment, comment_like, comment_saved};
apub::{make_apub_endpoint, EndpointType},
naive_now,
schema::{comment, comment_like, comment_saved},
};
// WITH RECURSIVE MyTree AS ( // WITH RECURSIVE MyTree AS (
// SELECT * FROM comment WHERE parent_id IS NULL // SELECT * FROM comment WHERE parent_id IS NULL
@ -77,12 +73,15 @@ impl Crud<CommentForm> for Comment {
} }
impl Comment { impl Comment {
pub fn update_ap_id(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> { pub fn update_ap_id(
conn: &PgConnection,
comment_id: i32,
apub_id: String,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
let apid = make_apub_endpoint(EndpointType::Comment, &comment_id.to_string()).to_string();
diesel::update(comment.find(comment_id)) diesel::update(comment.find(comment_id))
.set(ap_id.eq(apid)) .set(ap_id.eq(apub_id))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
@ -204,10 +203,8 @@ impl Saveable<CommentSavedForm> for CommentSaved {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{comment::*, community::*, post::*, tests::establish_unpooled_connection, user::*};
super::{community::*, post::*, user::*},
*,
};
#[test] #[test]
fn test_crud() { fn test_crud() {
let conn = establish_unpooled_connection(); let conn = establish_unpooled_connection();

@ -1,5 +1,5 @@
// TODO, remove the cross join here, just join to user directly // TODO, remove the cross join here, just join to user directly
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType}; use crate::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,6 +9,7 @@ table! {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
post_name -> Varchar,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Bool, removed -> Bool,
@ -27,6 +28,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
@ -44,6 +46,7 @@ table! {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
post_name -> Varchar,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Bool, removed -> Bool,
@ -62,6 +65,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
@ -82,6 +86,7 @@ pub struct CommentView {
pub id: i32, pub id: i32,
pub creator_id: i32, pub creator_id: i32,
pub post_id: i32, pub post_id: i32,
pub post_name: String,
pub parent_id: Option<i32>, pub parent_id: Option<i32>,
pub content: String, pub content: String,
pub removed: bool, pub removed: bool,
@ -100,6 +105,7 @@ pub struct CommentView {
pub creator_actor_id: String, pub creator_actor_id: String,
pub creator_local: bool, pub creator_local: bool,
pub creator_name: String, pub creator_name: String,
pub creator_published: chrono::NaiveDateTime,
pub creator_avatar: Option<String>, pub creator_avatar: Option<String>,
pub score: i64, pub score: i64,
pub upvotes: i64, pub upvotes: i64,
@ -295,6 +301,7 @@ table! {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
post_name -> Varchar,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Bool, removed -> Bool,
@ -314,6 +321,7 @@ table! {
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
creator_published -> Timestamp,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
downvotes -> BigInt, downvotes -> BigInt,
@ -334,6 +342,7 @@ pub struct ReplyView {
pub id: i32, pub id: i32,
pub creator_id: i32, pub creator_id: i32,
pub post_id: i32, pub post_id: i32,
pub post_name: String,
pub parent_id: Option<i32>, pub parent_id: Option<i32>,
pub content: String, pub content: String,
pub removed: bool, pub removed: bool,
@ -353,6 +362,7 @@ pub struct ReplyView {
pub creator_local: bool, pub creator_local: bool,
pub creator_name: String, pub creator_name: String,
pub creator_avatar: Option<String>, pub creator_avatar: Option<String>,
pub creator_published: chrono::NaiveDateTime,
pub score: i64, pub score: i64,
pub upvotes: i64, pub upvotes: i64,
pub downvotes: i64, pub downvotes: i64,
@ -455,11 +465,17 @@ impl<'a> ReplyQueryBuilder<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{comment::*, community::*, post::*, user::*}, comment::*,
comment_view::*,
community::*,
post::*,
tests::establish_unpooled_connection,
user::*,
Crud,
Likeable,
*, *,
}; };
use crate::db::{establish_unpooled_connection, Crud, Likeable};
#[test] #[test]
fn test_crud() { fn test_crud() {
@ -565,6 +581,7 @@ mod tests {
content: "A test comment 32".into(), content: "A test comment 32".into(),
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
post_name: inserted_post.name.to_owned(),
community_id: inserted_community.id, community_id: inserted_community.id,
community_name: inserted_community.name.to_owned(), community_name: inserted_community.name.to_owned(),
parent_id: None, parent_id: None,
@ -576,6 +593,7 @@ mod tests {
published: inserted_comment.published, published: inserted_comment.published,
updated: None, updated: None,
creator_name: inserted_user.name.to_owned(), creator_name: inserted_user.name.to_owned(),
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
score: 1, score: 1,
downvotes: 0, downvotes: 0,
@ -598,6 +616,7 @@ mod tests {
content: "A test comment 32".into(), content: "A test comment 32".into(),
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
post_name: inserted_post.name.to_owned(),
community_id: inserted_community.id, community_id: inserted_community.id,
community_name: inserted_community.name.to_owned(), community_name: inserted_community.name.to_owned(),
parent_id: None, parent_id: None,
@ -609,6 +628,7 @@ mod tests {
published: inserted_comment.published, published: inserted_comment.published,
updated: None, updated: None,
creator_name: inserted_user.name.to_owned(), creator_name: inserted_user.name.to_owned(),
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
score: 1, score: 1,
downvotes: 0, downvotes: 0,

@ -1,6 +1,9 @@
use crate::{ use crate::{
db::{Bannable, Crud, Followable, Joinable},
schema::{community, community_follower, community_moderator, community_user_ban}, schema::{community, community_follower, community_moderator, community_user_ban},
Bannable,
Crud,
Followable,
Joinable,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -232,8 +235,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use crate::{community::*, tests::establish_unpooled_connection, user::*, ListingType, SortType};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,5 +1,5 @@
use super::community_view::community_fast_view::BoxedQuery; use super::community_view::community_fast_view::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType}; use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{pg::Pg, result::Error, *}; use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -299,6 +299,7 @@ impl CommunityModeratorView {
use super::community_view::community_moderator_view::dsl::*; use super::community_view::community_moderator_view::dsl::*;
community_moderator_view community_moderator_view
.filter(community_id.eq(from_community_id)) .filter(community_id.eq(from_community_id))
.order_by(published)
.load::<Self>(conn) .load::<Self>(conn)
} }
@ -306,6 +307,7 @@ impl CommunityModeratorView {
use super::community_view::community_moderator_view::dsl::*; use super::community_view::community_moderator_view::dsl::*;
community_moderator_view community_moderator_view
.filter(user_id.eq(from_user_id)) .filter(user_id.eq(from_user_id))
.order_by(published)
.load::<Self>(conn) .load::<Self>(conn)
} }
} }

@ -1,10 +1,22 @@
use crate::settings::Settings; #[macro_use]
pub extern crate diesel;
#[macro_use]
pub extern crate strum_macros;
pub extern crate bcrypt;
pub extern crate chrono;
pub extern crate log;
pub extern crate serde;
pub extern crate serde_json;
pub extern crate sha2;
pub extern crate strum;
use chrono::NaiveDateTime;
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
pub mod activity; pub mod activity;
pub mod category; pub mod category;
pub mod code_migrations;
pub mod comment; pub mod comment;
pub mod comment_view; pub mod comment_view;
pub mod community; pub mod community;
@ -16,6 +28,7 @@ pub mod post;
pub mod post_view; pub mod post_view;
pub mod private_message; pub mod private_message;
pub mod private_message_view; pub mod private_message_view;
pub mod schema;
pub mod site; pub mod site;
pub mod site_view; pub mod site_view;
pub mod user; pub mod user;
@ -111,9 +124,8 @@ impl<T> MaybeOptional<T> for Option<T> {
} }
} }
pub fn establish_unpooled_connection() -> PgConnection { pub fn get_database_url_from_env() -> Result<String, VarError> {
let db_url = Settings::get().get_database_url(); env::var("LEMMY_DATABASE_URL")
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
} }
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)] #[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
@ -155,9 +167,25 @@ pub fn limit_and_offset(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
let offset = limit * (page - 1); let offset = limit * (page - 1);
(limit, offset) (limit, offset)
} }
pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::fuzzy_search; use super::fuzzy_search;
use crate::get_database_url_from_env;
use diesel::{Connection, PgConnection};
pub fn establish_unpooled_connection() -> PgConnection {
let db_url = match get_database_url_from_env() {
Ok(url) => url,
Err(e) => panic!("Failed to read database URL from env var LEMMY_DATABASE_URL: {}", e),
};
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
}
#[test] #[test]
fn test_fuzzy_search() { fn test_fuzzy_search() {
let test = "This is a fuzzy search"; let test = "This is a fuzzy search";

@ -1,5 +1,4 @@
use crate::{ use crate::{
db::Crud,
schema::{ schema::{
mod_add, mod_add,
mod_add_community, mod_add_community,
@ -11,6 +10,7 @@ use crate::{
mod_remove_post, mod_remove_post,
mod_sticky_post, mod_sticky_post,
}, },
Crud,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -437,11 +437,16 @@ impl Crud<ModAddForm> for ModAdd {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{comment::*, community::*, post::*, user::*}, comment::*,
*, community::*,
moderator::*,
post::*,
tests::establish_unpooled_connection,
user::*,
ListingType,
SortType,
}; };
use crate::db::{establish_unpooled_connection, ListingType, SortType};
// use Crud; // use Crud;
#[test] #[test]

@ -1,4 +1,4 @@
use crate::db::limit_and_offset; use crate::limit_and_offset;
use diesel::{result::Error, *}; use diesel::{result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

@ -1,6 +1,6 @@
use crate::{ use crate::{
db::Crud,
schema::{password_reset_request, password_reset_request::dsl::*}, schema::{password_reset_request, password_reset_request::dsl::*},
Crud,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -82,7 +82,7 @@ impl PasswordResetRequest {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use super::{super::user::*, *};
use crate::db::{establish_unpooled_connection, ListingType, SortType}; use crate::{tests::establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,8 +1,10 @@
use crate::{ use crate::{
apub::{make_apub_endpoint, EndpointType},
db::{Crud, Likeable, Readable, Saveable},
naive_now, naive_now,
schema::{post, post_like, post_read, post_saved}, schema::{post, post_like, post_read, post_saved},
Crud,
Likeable,
Readable,
Saveable,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -75,12 +77,11 @@ impl Post {
post.filter(ap_id.eq(object_id)).first::<Self>(conn) post.filter(ap_id.eq(object_id)).first::<Self>(conn)
} }
pub fn update_ap_id(conn: &PgConnection, post_id: i32) -> Result<Self, Error> { pub fn update_ap_id(conn: &PgConnection, post_id: i32, apub_id: String) -> Result<Self, Error> {
use crate::schema::post::dsl::*; use crate::schema::post::dsl::*;
let apid = make_apub_endpoint(EndpointType::Post, &post_id.to_string()).to_string();
diesel::update(post.find(post_id)) diesel::update(post.find(post_id))
.set(ap_id.eq(apid)) .set(ap_id.eq(apub_id))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
@ -241,11 +242,14 @@ impl Readable<PostReadForm> for PostRead {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{community::*, user::*}, community::*,
*, post::*,
tests::establish_unpooled_connection,
user::*,
ListingType,
SortType,
}; };
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,5 +1,5 @@
use super::post_view::post_fast_view::BoxedQuery; use super::post_view::post_fast_view::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType}; use crate::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -28,6 +28,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
banned -> Bool, banned -> Bool,
banned_from_community -> Bool, banned_from_community -> Bool,
@ -75,6 +76,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
banned -> Bool, banned -> Bool,
banned_from_community -> Bool, banned_from_community -> Bool,
@ -125,6 +127,7 @@ pub struct PostView {
pub creator_actor_id: String, pub creator_actor_id: String,
pub creator_local: bool, pub creator_local: bool,
pub creator_name: String, pub creator_name: String,
pub creator_published: chrono::NaiveDateTime,
pub creator_avatar: Option<String>, pub creator_avatar: Option<String>,
pub banned: bool, pub banned: bool,
pub banned_from_community: bool, pub banned_from_community: bool,
@ -155,6 +158,7 @@ pub struct PostQueryBuilder<'a> {
my_user_id: Option<i32>, my_user_id: Option<i32>,
for_creator_id: Option<i32>, for_creator_id: Option<i32>,
for_community_id: Option<i32>, for_community_id: Option<i32>,
for_community_name: Option<String>,
search_term: Option<String>, search_term: Option<String>,
url_search: Option<String>, url_search: Option<String>,
show_nsfw: bool, show_nsfw: bool,
@ -178,6 +182,7 @@ impl<'a> PostQueryBuilder<'a> {
my_user_id: None, my_user_id: None,
for_creator_id: None, for_creator_id: None,
for_community_id: None, for_community_id: None,
for_community_name: None,
search_term: None, search_term: None,
url_search: None, url_search: None,
show_nsfw: true, show_nsfw: true,
@ -203,6 +208,11 @@ impl<'a> PostQueryBuilder<'a> {
self self
} }
pub fn for_community_name<T: MaybeOptional<String>>(mut self, for_community_name: T) -> Self {
self.for_community_name = for_community_name.get_optional();
self
}
pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self { pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
self.for_creator_id = for_creator_id.get_optional(); self.for_creator_id = for_creator_id.get_optional();
self self
@ -262,6 +272,11 @@ impl<'a> PostQueryBuilder<'a> {
query = query.then_order_by(stickied.desc()); query = query.then_order_by(stickied.desc());
} }
if let Some(for_community_name) = self.for_community_name {
query = query.filter(community_name.eq(for_community_name));
query = query.then_order_by(stickied.desc());
}
if let Some(url_search) = self.url_search { if let Some(url_search) = self.url_search {
query = query.filter(url.eq(url_search)); query = query.filter(url.eq(url_search));
} }
@ -364,11 +379,16 @@ impl PostView {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{community::*, post::*, user::*}, community::*,
post::*,
post_view::*,
tests::establish_unpooled_connection,
user::*,
Crud,
Likeable,
*, *,
}; };
use crate::db::{establish_unpooled_connection, Crud, Likeable};
#[test] #[test]
fn test_crud() { fn test_crud() {
@ -499,6 +519,7 @@ mod tests {
body: None, body: None,
creator_id: inserted_user.id, creator_id: inserted_user.id,
creator_name: user_name.to_owned(), creator_name: user_name.to_owned(),
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
banned: false, banned: false,
banned_from_community: false, banned_from_community: false,
@ -548,6 +569,7 @@ mod tests {
stickied: false, stickied: false,
creator_id: inserted_user.id, creator_id: inserted_user.id,
creator_name: user_name, creator_name: user_name,
creator_published: inserted_user.published,
creator_avatar: None, creator_avatar: None,
banned: false, banned: false,
banned_from_community: false, banned_from_community: false,

@ -1,8 +1,4 @@
use crate::{ use crate::{schema::private_message, Crud};
apub::{make_apub_endpoint, EndpointType},
db::Crud,
schema::private_message,
};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -66,16 +62,15 @@ impl Crud<PrivateMessageForm> for PrivateMessage {
} }
impl PrivateMessage { impl PrivateMessage {
pub fn update_ap_id(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> { pub fn update_ap_id(
conn: &PgConnection,
private_message_id: i32,
apub_id: String,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*; use crate::schema::private_message::dsl::*;
let apid = make_apub_endpoint(
EndpointType::PrivateMessage,
&private_message_id.to_string(),
)
.to_string();
diesel::update(private_message.find(private_message_id)) diesel::update(private_message.find(private_message_id))
.set(ap_id.eq(apid)) .set(ap_id.eq(apub_id))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
@ -89,8 +84,13 @@ impl PrivateMessage {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{super::user::*, *}; use crate::{
use crate::db::{establish_unpooled_connection, ListingType, SortType}; private_message::*,
tests::establish_unpooled_connection,
user::*,
ListingType,
SortType,
};
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,4 +1,4 @@
use crate::db::{limit_and_offset, MaybeOptional}; use crate::{limit_and_offset, MaybeOptional};
use diesel::{pg::Pg, result::Error, *}; use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

@ -47,6 +47,7 @@ table! {
deleted -> Nullable<Bool>, deleted -> Nullable<Bool>,
ap_id -> Nullable<Varchar>, ap_id -> Nullable<Varchar>,
local -> Nullable<Bool>, local -> Nullable<Bool>,
post_name -> Nullable<Varchar>,
community_id -> Nullable<Int4>, community_id -> Nullable<Int4>,
community_actor_id -> Nullable<Varchar>, community_actor_id -> Nullable<Varchar>,
community_local -> Nullable<Bool>, community_local -> Nullable<Bool>,
@ -56,6 +57,7 @@ table! {
creator_actor_id -> Nullable<Varchar>, creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>, creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>, creator_name -> Nullable<Varchar>,
creator_published -> Nullable<Timestamp>,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
score -> Nullable<Int8>, score -> Nullable<Int8>,
upvotes -> Nullable<Int8>, upvotes -> Nullable<Int8>,
@ -317,6 +319,7 @@ table! {
creator_actor_id -> Nullable<Varchar>, creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>, creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>, creator_name -> Nullable<Varchar>,
creator_published -> Nullable<Timestamp>,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
banned -> Nullable<Bool>, banned -> Nullable<Bool>,
banned_from_community -> Nullable<Bool>, banned_from_community -> Nullable<Bool>,

@ -1,4 +1,4 @@
use crate::{db::Crud, schema::site}; use crate::{schema::site, Crud};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

@ -1,14 +1,10 @@
use crate::{ use crate::{
db::Crud,
is_email_regex,
naive_now, naive_now,
schema::{user_, user_::dsl::*}, schema::{user_, user_::dsl::*},
settings::Settings, Crud,
}; };
use bcrypt::{hash, DEFAULT_COST}; use bcrypt::{hash, DEFAULT_COST};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize};
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)] #[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "user_"] #[table_name = "user_"]
@ -131,90 +127,23 @@ impl User_ {
} }
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub id: i32,
pub username: String,
pub iss: String,
pub show_nsfw: bool,
pub theme: String,
pub default_sort_type: i16,
pub default_listing_type: i16,
pub lang: String,
pub avatar: Option<String>,
pub show_avatars: bool,
}
impl Claims {
pub fn decode(jwt: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
let v = Validation {
validate_exp: false,
..Validation::default()
};
decode::<Claims>(
&jwt,
&DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
&v,
)
}
}
type Jwt = String;
impl User_ { impl User_ {
pub fn jwt(&self) -> Jwt { pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
let my_claims = Claims {
id: self.id,
username: self.name.to_owned(),
iss: Settings::get().hostname,
show_nsfw: self.show_nsfw,
theme: self.theme.to_owned(),
default_sort_type: self.default_sort_type,
default_listing_type: self.default_listing_type,
lang: self.lang.to_owned(),
avatar: self.avatar.to_owned(),
show_avatars: self.show_avatars.to_owned(),
};
encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
)
.unwrap()
}
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
user_.filter(name.eq(username)).first::<User_>(conn) user_.filter(name.eq(username)).first::<User_>(conn)
} }
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> { pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<User_, Error> {
user_.filter(email.eq(from_email)).first::<User_>(conn) user_.filter(email.eq(from_email)).first::<User_>(conn)
} }
pub fn find_by_email_or_username( pub fn get_profile_url(&self, hostname: &str) -> String {
conn: &PgConnection, format!("https://{}/u/{}", hostname, self.name)
username_or_email: &str,
) -> Result<Self, Error> {
if is_email_regex(username_or_email) {
User_::find_by_email(conn, username_or_email)
} else {
User_::find_by_username(conn, username_or_email)
}
}
pub fn get_profile_url(&self) -> String {
format!("https://{}/u/{}", Settings::get().hostname, self.name)
}
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
Self::read(&conn, claims.id)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{User_, *}; use crate::{tests::establish_unpooled_connection, user::*, ListingType, SortType};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,5 +1,5 @@
use super::comment::Comment; use super::comment::Comment;
use crate::{db::Crud, schema::user_mention}; use crate::{schema::user_mention, Crud};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -54,11 +54,16 @@ impl Crud<UserMentionForm> for UserMention {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use crate::{
super::{comment::*, community::*, post::*, user::*}, comment::*,
*, community::*,
post::*,
tests::establish_unpooled_connection,
user::*,
user_mention::*,
ListingType,
SortType,
}; };
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test] #[test]
fn test_crud() { fn test_crud() {

@ -1,4 +1,4 @@
use crate::db::{limit_and_offset, MaybeOptional, SortType}; use crate::{limit_and_offset, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,6 +11,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
post_id -> Int4, post_id -> Int4,
post_name -> Varchar,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Bool, removed -> Bool,
@ -47,6 +48,7 @@ table! {
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
post_id -> Int4, post_id -> Int4,
post_name -> Varchar,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Bool, removed -> Bool,
@ -86,6 +88,7 @@ pub struct UserMentionView {
pub creator_actor_id: String, pub creator_actor_id: String,
pub creator_local: bool, pub creator_local: bool,
pub post_id: i32, pub post_id: i32,
pub post_name: String,
pub parent_id: Option<i32>, pub parent_id: Option<i32>,
pub content: String, pub content: String,
pub removed: bool, pub removed: bool,

@ -1,5 +1,5 @@
use super::user_view::user_fast::BoxedQuery; use super::user_view::user_fast::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType}; use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -157,7 +157,10 @@ impl UserView {
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_fast::dsl::*; use super::user_view::user_fast::dsl::*;
user_fast.filter(admin.eq(true)).load::<Self>(conn) user_fast
.filter(admin.eq(true))
.order_by(published)
.load::<Self>(conn)
} }
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {

@ -0,0 +1,22 @@
[package]
name = "lemmy_utils"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1.3.5"
config = { version = "0.10.1", default-features = false, features = ["hjson"] }
chrono = { version = "0.4.7", features = ["serde"] }
lettre = "0.9.3"
lettre_email = "0.9.4"
log = "0.4.0"
itertools = "0.9.0"
rand = "0.7.3"
serde = { version = "1.0.105", features = ["derive"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]}
comrak = "0.7"
lazy_static = "1.3.0"
openssl = "0.10"
url = { version = "2.1.1", features = ["serde"] }

@ -0,0 +1,337 @@
#[macro_use]
pub extern crate lazy_static;
pub extern crate comrak;
pub extern crate lettre;
pub extern crate lettre_email;
pub extern crate openssl;
pub extern crate rand;
pub extern crate regex;
pub extern crate serde_json;
pub extern crate url;
pub mod settings;
use crate::settings::Settings;
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use itertools::Itertools;
use lettre::{
smtp::{
authentication::{Credentials, Mechanism},
extension::ClientId,
ConnectionReuseParameters,
},
ClientSecurity,
SmtpClient,
Transport,
};
use lettre_email::Email;
use openssl::{pkey::PKey, rsa::Rsa};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Regex, RegexBuilder};
use std::io::{Error, ErrorKind};
use url::Url;
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
DateTime::<Utc>::from_utc(ndt, Utc)
}
pub fn naive_from_unix(time: i64) -> NaiveDateTime {
NaiveDateTime::from_timestamp(time, 0)
}
pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
let now = Local::now();
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub fn remove_slurs(test: &str) -> String {
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
pub fn slur_check(test: &str) -> Result<(), Vec<&str>> {
let mut matches: Vec<&str> = SLUR_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
// Unique
matches.sort_unstable();
matches.dedup();
if matches.is_empty() {
Ok(())
} else {
Err(matches)
}
}
pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
let start = "No slurs - ";
let combined = &slurs.join(", ");
[start, combined].concat()
}
pub fn generate_random_string() -> String {
thread_rng().sample_iter(&Alphanumeric).take(30).collect()
}
pub fn send_email(
subject: &str,
to_email: &str,
to_username: &str,
html: &str,
) -> Result<(), String> {
let email_config = Settings::get().email.ok_or("no_email_setup")?;
let email = Email::builder()
.to((to_email, to_username))
.from(email_config.smtp_from_address.to_owned())
.subject(subject)
.html(html)
.build()
.unwrap();
let mailer = if email_config.use_tls {
SmtpClient::new_simple(&email_config.smtp_server).unwrap()
} else {
SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
}
.hello_name(ClientId::Domain(Settings::get().hostname))
.smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited);
let mailer = if let (Some(login), Some(password)) =
(&email_config.smtp_login, &email_config.smtp_password)
{
mailer.credentials(Credentials::new(login.to_owned(), password.to_owned()))
} else {
mailer
};
let mut transport = mailer.transport();
let result = transport.send(email.into());
transport.close();
match result {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
pub fn markdown_to_html(text: &str) -> String {
comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
}
// TODO nothing is done with community / group webfingers yet, so just ignore those for now
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct MentionData {
pub name: String,
pub domain: String,
}
impl MentionData {
pub fn is_local(&self) -> bool {
Settings::get().hostname.eq(&self.domain)
}
pub fn full_name(&self) -> String {
format!("@{}@{}", &self.name, &self.domain)
}
}
pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
let mut out: Vec<MentionData> = Vec::new();
for caps in MENTIONS_REGEX.captures_iter(text) {
out.push(MentionData {
name: caps["name"].to_string(),
domain: caps["domain"].to_string(),
});
}
out.into_iter().unique().collect()
}
pub fn is_valid_username(name: &str) -> bool {
VALID_USERNAME_REGEX.is_match(name)
}
pub fn is_valid_community_name(name: &str) -> bool {
VALID_COMMUNITY_NAME_REGEX.is_match(name)
}
pub fn is_valid_post_title(title: &str) -> bool {
VALID_POST_TITLE_REGEX.is_match(title)
}
#[cfg(test)]
mod tests {
use crate::{
is_email_regex,
is_valid_community_name,
is_valid_post_title,
is_valid_username,
remove_slurs,
scrape_text_for_mentions,
slur_check,
slurs_vec_to_str,
};
#[test]
fn test_mentions_regex() {
let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)";
let mentions = scrape_text_for_mentions(text);
assert_eq!(mentions[0].name, "tedu".to_string());
assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
}
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test]
fn test_valid_register_username() {
assert!(is_valid_username("Hello_98"));
assert!(is_valid_username("ten"));
assert!(!is_valid_username("Hello-98"));
assert!(!is_valid_username("a"));
assert!(!is_valid_username(""));
}
#[test]
fn test_valid_community_name() {
assert!(is_valid_community_name("example"));
assert!(is_valid_community_name("example_community"));
assert!(!is_valid_community_name("Example"));
assert!(!is_valid_community_name("Ex"));
assert!(!is_valid_community_name(""));
}
#[test]
fn test_valid_post_title() {
assert!(is_valid_post_title("Post Title"));
assert!(is_valid_post_title(" POST TITLE 😃😃😃😃😃"));
assert!(!is_valid_post_title("\n \n \n \n ")); // tabs/spaces/newlines
}
#[test]
fn test_slur_filter() {
let test =
"coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
let slur_free = "No slurs here";
assert_eq!(
remove_slurs(&test),
"*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
.to_string()
);
let has_slurs_vec = vec![
"Niggerz",
"coons",
"dindu",
"ladyboy",
"retardeds",
"tranny",
];
let has_slurs_err_str = "No slurs - Niggerz, coons, dindu, ladyboy, retardeds, tranny";
assert_eq!(slur_check(test), Err(has_slurs_vec));
assert_eq!(slur_check(slur_free), Ok(()));
if let Err(slur_vec) = slur_check(test) {
assert_eq!(&slurs_vec_to_str(slur_vec), has_slurs_err_str);
}
}
// These helped with testing
// #[test]
// fn test_send_email() {
// let result = send_email("not a subject", "test_email@gmail.com", "ur user", "<h1>HI there</h1>");
// assert!(result.is_ok());
// }
}
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 = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bn(i|1)g(\b|g?(a|er)?(s|z)?)\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)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
// TODO keep this old one, it didn't work with port well tho
// static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
static ref VALID_POST_TITLE_REGEX: Regex = Regex::new(r".*\S.*").unwrap();
pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
"^group:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
pub static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
"^acct:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
pub static ref CACHE_CONTROL_REGEX: Regex =
Regex::new("^((text|image)/.+|application/javascript)$").unwrap();
}
pub struct Keypair {
pub private_key: String,
pub public_key: String,
}
/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn generate_actor_keypair() -> Result<Keypair, Error> {
let rsa = Rsa::generate(2048)?;
let pkey = PKey::from_rsa(rsa)?;
let public_key = pkey.public_key_to_pem()?;
let private_key = pkey.private_key_to_pem_pkcs8()?;
let key_to_string = |key| match String::from_utf8(key) {
Ok(s) => Ok(s),
Err(e) => Err(Error::new(
ErrorKind::Other,
format!("Failed converting key to string: {}", e),
)),
};
Ok(Keypair {
private_key: key_to_string(private_key)?,
public_key: key_to_string(public_key)?,
})
}
pub enum EndpointType {
Community,
User,
Post,
Comment,
PrivateMessage,
}
pub fn get_apub_protocol_string() -> &'static str {
if Settings::get().federation.tls_enabled {
"https"
} else {
"http"
}
}
/// Generates the ActivityPub ID for a given object type and ID.
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::User => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Url::parse(&format!(
"{}://{}/{}/{}",
get_apub_protocol_string(),
Settings::get().hostname,
point,
name
))
.unwrap()
}

@ -1,7 +1,7 @@
use crate::LemmyError;
use config::{Config, ConfigError, Environment, File}; use config::{Config, ConfigError, Environment, File};
use serde::Deserialize; use serde::Deserialize;
use std::{env, fs, net::IpAddr, sync::RwLock}; use std::{fs, io::Error, net::IpAddr, sync::RwLock};
use std::env;
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson"; static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
static CONFIG_FILE: &str = "config/config.hjson"; static CONFIG_FILE: &str = "config/config.hjson";
@ -76,12 +76,15 @@ impl Settings {
/// First, defaults are loaded from CONFIG_FILE_DEFAULTS, then these values can be overwritten /// First, defaults are loaded from CONFIG_FILE_DEFAULTS, then these values can be overwritten
/// from CONFIG_FILE (optional). Finally, values from the environment (with prefix LEMMY) are /// from CONFIG_FILE (optional). Finally, values from the environment (with prefix LEMMY) are
/// added to the config. /// added to the config.
///
/// Note: The env var `LEMMY_DATABASE_URL` is parsed in
/// `server/lemmy_db/src/lib.rs::get_database_url_from_env()`
fn init() -> Result<Self, ConfigError> { fn init() -> Result<Self, ConfigError> {
let mut s = Config::new(); let mut s = Config::new();
s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?; s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?;
s.merge(File::with_name(CONFIG_FILE).required(false))?; s.merge(File::with_name(&Self::get_config_location()).required(false))?;
// Add in settings from the environment (with a prefix of LEMMY) // Add in settings from the environment (with a prefix of LEMMY)
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key // Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
@ -98,32 +101,31 @@ impl Settings {
SETTINGS.read().unwrap().to_owned() SETTINGS.read().unwrap().to_owned()
} }
/// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used,
/// otherwise the connection url is generated from the config.
pub fn get_database_url(&self) -> String { pub fn get_database_url(&self) -> String {
match env::var("LEMMY_DATABASE_URL") { format!(
Ok(url) => url, "postgres://{}:{}@{}:{}/{}",
Err(_) => format!( self.database.user,
"postgres://{}:{}@{}:{}/{}", self.database.password,
self.database.user, self.database.host,
self.database.password, self.database.port,
self.database.host, self.database.database
self.database.port, )
self.database.database
),
}
} }
pub fn api_endpoint(&self) -> String { pub fn api_endpoint(&self) -> String {
format!("{}/api/v1", self.hostname) format!("{}/api/v1", self.hostname)
} }
pub fn read_config_file() -> Result<String, LemmyError> { pub fn get_config_location() -> String {
Ok(fs::read_to_string(CONFIG_FILE)?) env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE.to_string())
} }
pub fn save_config_file(data: &str) -> Result<String, LemmyError> { pub fn read_config_file() -> Result<String, Error> {
fs::write(CONFIG_FILE, data)?; fs::read_to_string(Self::get_config_location())
}
pub fn save_config_file(data: &str) -> Result<String, Error> {
fs::write(Self::get_config_location(), data)?;
// Reload the new settings // Reload the new settings
// From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804 // From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804

@ -0,0 +1,388 @@
drop view user_mention_view;
drop view reply_fast_view;
drop view comment_fast_view;
drop view comment_view;
drop view user_mention_fast_view;
drop table comment_aggregates_fast;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- community details
p.community_id,
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_fast_view cv, closereply
where closereply.id = cv.id
;
-- add creator_published to the post view
drop view post_fast_view;
drop table post_aggregates_fast;
drop view post_view;
drop view post_aggregates_view;
create view post_aggregates_view as
select
p.*,
-- creator details
u.actor_id as creator_actor_id,
u."local" as creator_local,
u."name" as creator_name,
u.avatar as creator_avatar,
u.banned as banned,
cb.id::bool as banned_from_community,
-- community details
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
c.removed as community_removed,
c.deleted as community_deleted,
c.nsfw as community_nsfw,
-- post score data/comment count
coalesce(ct.comments, 0) as number_of_comments,
coalesce(pl.score, 0) as score,
coalesce(pl.upvotes, 0) as upvotes,
coalesce(pl.downvotes, 0) as downvotes,
hot_rank(
coalesce(pl.score , 0), (
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join user_ u on p.creator_id = u.id
left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
left join community c on p.community_id = c.id
left join (
select
post_id,
count(*) as comments,
max(published) as recent_comment_time
from comment
group by post_id
) ct on ct.post_id = p.id
left join (
select
post_id,
sum(score) as score,
sum(score) filter (where score = 1) as upvotes,
-sum(score) filter (where score = -1) as downvotes
from post_like
group by post_id
) pl on pl.post_id = p.id
order by p.id;
create view post_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_view pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_view pav;
create table post_aggregates_fast as select * from post_aggregates_view;
alter table post_aggregates_fast add primary key (id);
create view post_fast_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_fast pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_fast pav;

@ -0,0 +1,390 @@
drop view user_mention_view;
drop view reply_fast_view;
drop view comment_fast_view;
drop view comment_view;
drop view user_mention_fast_view;
drop table comment_aggregates_fast;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- community details
p.community_id,
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.published as creator_published,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_fast_view cv, closereply
where closereply.id = cv.id
;
-- add creator_published to the post view
drop view post_fast_view;
drop table post_aggregates_fast;
drop view post_view;
drop view post_aggregates_view;
create view post_aggregates_view as
select
p.*,
-- creator details
u.actor_id as creator_actor_id,
u."local" as creator_local,
u."name" as creator_name,
u.published as creator_published,
u.avatar as creator_avatar,
u.banned as banned,
cb.id::bool as banned_from_community,
-- community details
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
c.removed as community_removed,
c.deleted as community_deleted,
c.nsfw as community_nsfw,
-- post score data/comment count
coalesce(ct.comments, 0) as number_of_comments,
coalesce(pl.score, 0) as score,
coalesce(pl.upvotes, 0) as upvotes,
coalesce(pl.downvotes, 0) as downvotes,
hot_rank(
coalesce(pl.score , 0), (
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join user_ u on p.creator_id = u.id
left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
left join community c on p.community_id = c.id
left join (
select
post_id,
count(*) as comments,
max(published) as recent_comment_time
from comment
group by post_id
) ct on ct.post_id = p.id
left join (
select
post_id,
sum(score) as score,
sum(score) filter (where score = 1) as upvotes,
-sum(score) filter (where score = -1) as downvotes
from post_like
group by post_id
) pl on pl.post_id = p.id
order by p.id;
create view post_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_view pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_view pav;
create table post_aggregates_fast as select * from post_aggregates_view;
alter table post_aggregates_fast add primary key (id);
create view post_fast_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_fast pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_fast pav;

@ -0,0 +1,249 @@
drop view user_mention_view;
drop view reply_fast_view;
drop view comment_fast_view;
drop view comment_view;
drop view user_mention_fast_view;
drop table comment_aggregates_fast;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- community details
p.community_id,
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.published as creator_published,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_fast_view cv, closereply
where closereply.id = cv.id
;

@ -0,0 +1,254 @@
drop view user_mention_view;
drop view reply_fast_view;
drop view comment_fast_view;
drop view comment_view;
drop view user_mention_fast_view;
drop table comment_aggregates_fast;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- post details
p."name" as post_name,
p.community_id,
-- community details
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.published as creator_published,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.post_name,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.post_name,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.post_name,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_fast_view cv, closereply
where closereply.id = cv.id
;

@ -0,0 +1,73 @@
use diesel::{result::Error, PgConnection};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use lemmy_db::{user::User_, Crud};
use lemmy_utils::{is_email_regex, settings::Settings};
use serde::{Deserialize, Serialize};
type Jwt = String;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub id: i32,
pub username: String,
pub iss: String,
pub show_nsfw: bool,
pub theme: String,
pub default_sort_type: i16,
pub default_listing_type: i16,
pub lang: String,
pub avatar: Option<String>,
pub show_avatars: bool,
}
impl Claims {
pub fn decode(jwt: &str) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> {
let v = Validation {
validate_exp: false,
..Validation::default()
};
decode::<Claims>(
&jwt,
&DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
&v,
)
}
pub fn jwt(user: User_, hostname: String) -> Jwt {
let my_claims = Claims {
id: user.id,
username: user.name.to_owned(),
iss: hostname,
show_nsfw: user.show_nsfw,
theme: user.theme.to_owned(),
default_sort_type: user.default_sort_type,
default_listing_type: user.default_listing_type,
lang: user.lang.to_owned(),
avatar: user.avatar.to_owned(),
show_avatars: user.show_avatars.to_owned(),
};
encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
)
.unwrap()
}
// TODO: move these into user?
pub fn find_by_email_or_username(
conn: &PgConnection,
username_or_email: &str,
) -> Result<User_, Error> {
if is_email_regex(username_or_email) {
User_::find_by_email(conn, username_or_email)
} else {
User_::find_by_username(conn, username_or_email)
}
}
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
User_::read(&conn, claims.id)
}
}

@ -1,28 +1,7 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking, blocking,
db::{
comment::*,
comment_view::*,
community_view::*,
moderator::*,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
},
naive_now,
remove_slurs,
scrape_text_for_mentions,
send_email,
settings::Settings,
websocket::{ websocket::{
server::{JoinCommunityRoom, SendComment}, server::{JoinCommunityRoom, SendComment},
UserOperation, UserOperation,
@ -30,6 +9,31 @@ use crate::{
}, },
DbPool, DbPool,
LemmyError, LemmyError,
};
use lemmy_db::{
comment::*,
comment_view::*,
community_view::*,
moderator::*,
naive_now,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
};
use lemmy_utils::{
make_apub_endpoint,
remove_slurs,
scrape_text_for_mentions,
send_email,
settings::Settings,
EndpointType,
MentionData, MentionData,
}; };
use log::error; use log::error;
@ -155,7 +159,9 @@ impl Perform for Oper<CreateComment> {
let inserted_comment_id = inserted_comment.id; let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| { let updated_comment: Comment = match blocking(pool, move |conn| {
Comment::update_ap_id(&conn, inserted_comment_id) let apub_id =
make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
}) })
.await? .await?
{ {

@ -1,26 +1,24 @@
use super::*; use super::*;
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ apub::ActorType,
extensions::signatures::generate_actor_keypair,
make_apub_endpoint,
ActorType,
EndpointType,
},
blocking, blocking,
db::{Bannable, Crud, Followable, Joinable, SortType},
is_valid_community_name,
naive_from_unix,
naive_now,
slur_check,
slurs_vec_to_str,
websocket::{ websocket::{
server::{JoinCommunityRoom, SendCommunityRoomMessage}, server::{JoinCommunityRoom, SendCommunityRoomMessage},
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool, DbPool,
LemmyError, };
use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType};
use lemmy_utils::{
generate_actor_keypair,
is_valid_community_name,
make_apub_endpoint,
naive_from_unix,
slur_check,
slurs_vec_to_str,
EndpointType,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;

@ -1,11 +1,8 @@
use crate::{ use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
websocket::WebsocketInfo,
DbPool,
LemmyError,
};
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
pub mod claims;
pub mod comment; pub mod comment;
pub mod community; pub mod community;
pub mod post; pub mod post;

@ -1,27 +1,8 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking, blocking,
db::{
comment_view::*,
community_view::*,
moderator::*,
post::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
},
fetch_iframely_and_pictrs_data, fetch_iframely_and_pictrs_data,
naive_now,
slur_check,
slurs_vec_to_str,
websocket::{ websocket::{
server::{JoinCommunityRoom, JoinPostRoom, SendPost}, server::{JoinCommunityRoom, JoinPostRoom, SendPost},
UserOperation, UserOperation,
@ -30,6 +11,30 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use lemmy_db::{
comment_view::*,
community_view::*,
moderator::*,
naive_now,
post::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
};
use lemmy_utils::{
is_valid_post_title,
make_apub_endpoint,
slur_check,
slurs_vec_to_str,
EndpointType,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -71,6 +76,7 @@ pub struct GetPosts {
page: Option<i64>, page: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
pub community_id: Option<i32>, pub community_id: Option<i32>,
pub community_name: Option<String>,
auth: Option<String>, auth: Option<String>,
} }
@ -136,6 +142,10 @@ impl Perform for Oper<CreatePost> {
} }
} }
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
}
let user_id = claims.id; let user_id = claims.id;
// Check for a community ban // Check for a community ban
@ -157,7 +167,7 @@ impl Perform for Oper<CreatePost> {
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.trim().to_owned(),
url: data.url.to_owned(), url: data.url.to_owned(),
body: data.body.to_owned(), body: data.body.to_owned(),
community_id: data.community_id, community_id: data.community_id,
@ -191,11 +201,16 @@ impl Perform for Oper<CreatePost> {
}; };
let inserted_post_id = inserted_post.id; let inserted_post_id = inserted_post.id;
let updated_post = let updated_post = match blocking(pool, move |conn| {
match blocking(pool, move |conn| Post::update_ap_id(conn, inserted_post_id)).await? { let apub_id =
Ok(post) => post, make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
Err(_e) => return Err(APIError::err("couldnt_create_post").into()), Post::update_ap_id(conn, inserted_post_id, apub_id)
}; })
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
};
updated_post.send_create(&user, &self.client, pool).await?; updated_post.send_create(&user, &self.client, pool).await?;
@ -361,12 +376,14 @@ impl Perform for Oper<GetPosts> {
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;
let community_id = data.community_id; let community_id = data.community_id;
let community_name = data.community_name.to_owned();
let posts = match blocking(pool, move |conn| { let posts = match blocking(pool, move |conn| {
PostQueryBuilder::create(conn) PostQueryBuilder::create(conn)
.listing_type(type_) .listing_type(type_)
.sort(&sort) .sort(&sort)
.show_nsfw(show_nsfw) .show_nsfw(show_nsfw)
.for_community_id(community_id) .for_community_id(community_id)
.for_community_name(community_name)
.my_user_id(user_id) .my_user_id(user_id)
.page(page) .page(page)
.limit(limit) .limit(limit)
@ -512,6 +529,10 @@ impl Perform for Oper<EditPost> {
} }
} }
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
}
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()), Err(_e) => return Err(APIError::err("not_logged_in").into()),
@ -561,7 +582,7 @@ impl Perform for Oper<EditPost> {
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.trim().to_owned(),
url: data.url.to_owned(), url: data.url.to_owned(),
body: data.body.to_owned(), body: data.body.to_owned(),
creator_id: data.creator_id.to_owned(), creator_id: data.creator_id.to_owned(),

@ -1,31 +1,28 @@
use super::user::Register; use super::user::Register;
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::fetcher::search_by_apub_id, apub::fetcher::search_by_apub_id,
blocking, blocking,
db::{
category::*,
comment_view::*,
community_view::*,
moderator::*,
moderator_views::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
SearchType,
SortType,
},
naive_now,
settings::Settings,
slur_check,
slurs_vec_to_str,
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo}, websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use lemmy_db::{
category::*,
comment_view::*,
community_view::*,
moderator::*,
moderator_views::*,
naive_now,
post_view::*,
site::*,
site_view::*,
user_view::*,
Crud,
SearchType,
SortType,
};
use lemmy_utils::{settings::Settings, slur_check, slurs_vec_to_str};
use log::{debug, info}; use log::{debug, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;

@ -1,53 +1,53 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{claims::Claims, APIError, Oper, Perform},
apub::{ apub::ApubObjectType,
extensions::signatures::generate_actor_keypair,
make_apub_endpoint,
ApubObjectType,
EndpointType,
},
blocking, blocking,
db::{ websocket::{
comment::*, server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
comment_view::*, UserOperation,
community::*, WebsocketInfo,
community_view::*,
moderator::*,
password_reset_request::*,
post::*,
post_view::*,
private_message::*,
private_message_view::*,
site::*,
site_view::*,
user::*,
user_mention::*,
user_mention_view::*,
user_view::*,
Crud,
Followable,
Joinable,
ListingType,
SortType,
}, },
DbPool,
LemmyError,
};
use bcrypt::verify;
use lemmy_db::{
comment::*,
comment_view::*,
community::*,
community_view::*,
moderator::*,
naive_now,
password_reset_request::*,
post::*,
post_view::*,
private_message::*,
private_message_view::*,
site::*,
site_view::*,
user::*,
user_mention::*,
user_mention_view::*,
user_view::*,
Crud,
Followable,
Joinable,
ListingType,
SortType,
};
use lemmy_utils::{
generate_actor_keypair,
generate_random_string, generate_random_string,
is_valid_username, is_valid_username,
make_apub_endpoint,
naive_from_unix, naive_from_unix,
naive_now,
remove_slurs, remove_slurs,
send_email, send_email,
settings::Settings, settings::Settings,
slur_check, slur_check,
slurs_vec_to_str, slurs_vec_to_str,
websocket::{ EndpointType,
server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
UserOperation,
WebsocketInfo,
},
DbPool,
LemmyError,
}; };
use bcrypt::verify;
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -264,7 +264,7 @@ impl Perform for Oper<Login> {
// Fetch that username / email // Fetch that username / email
let username_or_email = data.username_or_email.clone(); let username_or_email = data.username_or_email.clone();
let user = match blocking(pool, move |conn| { let user = match blocking(pool, move |conn| {
User_::find_by_email_or_username(conn, &username_or_email) Claims::find_by_email_or_username(conn, &username_or_email)
}) })
.await? .await?
{ {
@ -279,7 +279,9 @@ impl Perform for Oper<Login> {
} }
// Return the jwt // Return the jwt
Ok(LoginResponse { jwt: user.jwt() }) Ok(LoginResponse {
jwt: Claims::jwt(user, Settings::get().hostname),
})
} }
} }
@ -421,7 +423,7 @@ impl Perform for Oper<Register> {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: inserted_user.jwt(), jwt: Claims::jwt(inserted_user, Settings::get().hostname),
}) })
} }
} }
@ -451,6 +453,11 @@ impl Perform for Oper<SaveUserSettings> {
None => read_user.email, None => read_user.email,
}; };
let avatar = match &data.avatar {
Some(avatar) => Some(avatar.to_owned()),
None => read_user.avatar,
};
let password_encrypted = match &data.new_password { let password_encrypted = match &data.new_password {
Some(new_password) => { Some(new_password) => {
match &data.new_password_verify { match &data.new_password_verify {
@ -488,7 +495,7 @@ impl Perform for Oper<SaveUserSettings> {
name: read_user.name, name: read_user.name,
email, email,
matrix_user_id: data.matrix_user_id.to_owned(), matrix_user_id: data.matrix_user_id.to_owned(),
avatar: data.avatar.to_owned(), avatar,
password_encrypted, password_encrypted,
preferred_username: read_user.preferred_username, preferred_username: read_user.preferred_username,
updated: Some(naive_now()), updated: Some(naive_now()),
@ -527,7 +534,7 @@ impl Perform for Oper<SaveUserSettings> {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: updated_user.jwt(), jwt: Claims::jwt(updated_user, Settings::get().hostname),
}) })
} }
} }
@ -1150,7 +1157,7 @@ impl Perform for Oper<PasswordChange> {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: updated_user.jwt(), jwt: Claims::jwt(updated_user, Settings::get().hostname),
}) })
} }
} }
@ -1208,7 +1215,12 @@ impl Perform for Oper<CreatePrivateMessage> {
let inserted_private_message_id = inserted_private_message.id; let inserted_private_message_id = inserted_private_message.id;
let updated_private_message = match blocking(pool, move |conn| { let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update_ap_id(&conn, inserted_private_message_id) let apub_id = make_apub_endpoint(
EndpointType::PrivateMessage,
&inserted_private_message_id.to_string(),
)
.to_string();
PrivateMessage::update_ap_id(&conn, inserted_private_message_id, apub_id)
}) })
.await? .await?
{ {

@ -1,12 +1,18 @@
use crate::{ use crate::{
apub::{extensions::signatures::sign, is_apub_id_valid, ActorType}, apub::{
db::{activity::insert_activity, community::Community, user::User_}, community::do_announce,
extensions::signatures::sign,
insert_activity,
is_apub_id_valid,
ActorType,
},
request::retry_custom, request::retry_custom,
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{community::Community, user::User_};
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug; use std::fmt::Debug;
@ -43,7 +49,7 @@ where
// if this is a local community, we need to do an announce from the community instead // if this is a local community, we need to do an announce from the community instead
if community.local { if community.local {
Community::do_announce(activity, &community, creator, client, pool).await?; do_announce(activity, &community, creator, client, pool).await?;
} else { } else {
send_activity(client, &activity, creator, to).await?; send_activity(client, &activity, creator, to).await?;
} }

@ -1,35 +1,16 @@
use crate::{ use crate::{
apub::{ apub::{
activities::{populate_object_props, send_activity_to_community}, activities::{populate_object_props, send_activity_to_community},
create_apub_response, create_apub_response, create_apub_tombstone_response, create_tombstone, fetch_webfinger_url,
create_apub_tombstone_response,
create_tombstone,
fetch_webfinger_url,
fetcher::{ fetcher::{
get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post,
get_or_fetch_and_insert_remote_post,
get_or_fetch_and_upsert_remote_user, get_or_fetch_and_upsert_remote_user,
}, },
ActorType, ActorType, ApubLikeableType, ApubObjectType, FromApub, ToApub,
ApubLikeableType,
ApubObjectType,
FromApub,
ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
comment::{Comment, CommentForm},
community::Community,
post::Post,
user::User_,
Crud,
},
routes::DbPoolParam, routes::DbPoolParam,
scrape_text_for_mentions, DbPool, LemmyError,
DbPool,
LemmyError,
MentionData,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
@ -40,6 +21,14 @@ use activitystreams::{
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, client::Client, web::Path, HttpResponse}; use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
use itertools::Itertools; use itertools::Itertools;
use lemmy_db::{
comment::{Comment, CommentForm},
community::Community,
post::Post,
user::User_,
Crud,
};
use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
@ -123,7 +112,7 @@ impl FromApub for CommentForm {
/// Parse an ActivityPub note received from another instance into a Lemmy comment /// Parse an ActivityPub note received from another instance into a Lemmy comment
async fn from_apub( async fn from_apub(
note: &mut Note, note: &Note,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<CommentForm, LemmyError> { ) -> Result<CommentForm, LemmyError> {

@ -1,35 +1,18 @@
use crate::{ use crate::{
apub::{ apub::{
activities::{populate_object_props, send_activity}, activities::{populate_object_props, send_activity},
create_apub_response, create_apub_response, create_apub_tombstone_response, create_tombstone,
create_apub_tombstone_response,
create_tombstone,
extensions::group_extensions::GroupExtension, extensions::group_extensions::GroupExtension,
fetcher::get_or_fetch_and_upsert_remote_user, fetcher::get_or_fetch_and_upsert_remote_user,
get_shared_inbox, get_shared_inbox, insert_activity, ActorType, FromApub, GroupExt, ToApub,
ActorType,
FromApub,
GroupExt,
ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
activity::insert_activity,
community::{Community, CommunityForm},
community_view::{CommunityFollowerView, CommunityModeratorView},
user::User_,
},
naive_now,
routes::DbPoolParam, routes::DbPoolParam,
DbPool, DbPool, LemmyError,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, Announce, Delete, Remove, Undo}, activity::{Accept, Announce, Delete, Remove, Undo},
Activity, Activity, Base, BaseBox,
Base,
BaseBox,
}; };
use activitystreams_ext::Ext2; use activitystreams_ext::Ext2;
use activitystreams_new::{ use activitystreams_new::{
@ -44,6 +27,13 @@ use activitystreams_new::{
}; };
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use itertools::Itertools; use itertools::Itertools;
use lemmy_db::{
community::{Community, CommunityForm},
community_view::{CommunityFollowerView, CommunityModeratorView},
naive_now,
user::User_,
};
use lemmy_utils::convert_datetime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Debug, str::FromStr}; use std::{fmt::Debug, str::FromStr};
@ -367,13 +357,8 @@ impl FromApub for CommunityForm {
type ApubType = GroupExt; type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community. /// Parse an ActivityPub group received from another instance into a Lemmy community.
async fn from_apub( async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
group: &mut GroupExt, let creator_and_moderator_uris = group.attributed_to().unwrap();
client: &Client,
pool: &DbPool,
) -> Result<Self, LemmyError> {
// TODO: this is probably gonna cause problems cause fetcher:292 also calls take_attributed_to()
let creator_and_moderator_uris = group.clone().take_attributed_to().unwrap();
let creator_uri = creator_and_moderator_uris let creator_uri = creator_and_moderator_uris
.as_many() .as_many()
.unwrap() .unwrap()
@ -386,27 +371,20 @@ impl FromApub for CommunityForm {
let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?; let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
Ok(CommunityForm { Ok(CommunityForm {
name: group name: group.name().unwrap().as_single_xsd_string().unwrap().into(),
.take_name() title: group.inner.preferred_username().unwrap().to_string(),
.unwrap()
.as_single_xsd_string()
.unwrap()
.into(),
title: group.inner.take_preferred_username().unwrap(),
// TODO: should be parsed as html and tags like <script> removed (or use markdown source) // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
// -> same for post.content etc // -> same for post.content etc
description: group description: group
.take_content() .content()
.map(|s| s.as_single_xsd_string().unwrap().into()), .map(|s| s.as_single_xsd_string().unwrap().into()),
category_id: group.ext_one.category.identifier.parse::<i32>()?, category_id: group.ext_one.category.identifier.parse::<i32>()?,
creator_id: creator.id, creator_id: creator.id,
removed: None, removed: None,
published: group published: group
.take_published() .published()
.map(|u| u.as_ref().to_owned().naive_local()),
updated: group
.take_updated()
.map(|u| u.as_ref().to_owned().naive_local()), .map(|u| u.as_ref().to_owned().naive_local()),
updated: group.updated().map(|u| u.as_ref().to_owned().naive_local()),
deleted: None, deleted: None,
nsfw: group.ext_one.sensitive, nsfw: group.ext_one.sensitive,
actor_id: group.id().unwrap().to_string(), actor_id: group.id().unwrap().to_string(),
@ -462,39 +440,37 @@ pub async fn get_apub_community_followers(
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
impl Community { pub async fn do_announce<A>(
pub async fn do_announce<A>( activity: A,
activity: A, community: &Community,
community: &Community, sender: &dyn ActorType,
sender: &dyn ActorType, client: &Client,
client: &Client, pool: &DbPool,
pool: &DbPool, ) -> Result<HttpResponse, LemmyError>
) -> Result<HttpResponse, LemmyError> where
where A: Activity + Base + Serialize + Debug,
A: Activity + Base + Serialize + Debug, {
{ let mut announce = Announce::default();
let mut announce = Announce::default(); populate_object_props(
populate_object_props( &mut announce.object_props,
&mut announce.object_props, vec![community.get_followers_url()],
vec![community.get_followers_url()], &format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
&format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()), )?;
)?; announce
announce .announce_props
.announce_props .set_actor_xsd_any_uri(community.actor_id.to_owned())?
.set_actor_xsd_any_uri(community.actor_id.to_owned())? .set_object_base_box(BaseBox::from_concrete(activity)?)?;
.set_object_base_box(BaseBox::from_concrete(activity)?)?;
insert_activity(community.creator_id, announce.clone(), true, pool).await?;
insert_activity(community.creator_id, announce.clone(), true, pool).await?;
// dont send to the instance where the activity originally came from, because that would result
// dont send to the instance where the activity originally came from, because that would result // in a database error (same data inserted twice)
// in a database error (same data inserted twice) let mut to = community.get_follower_inboxes(pool).await?;
let mut to = community.get_follower_inboxes(pool).await?;
// this seems to be the "easiest" stable alternative for remove_item()
// this seems to be the "easiest" stable alternative for remove_item() to.retain(|x| *x != sender.get_shared_inbox_url());
to.retain(|x| *x != sender.get_shared_inbox_url());
send_activity(client, &announce, community, to).await?;
send_activity(client, &announce, community, to).await?;
Ok(HttpResponse::Ok().finish())
Ok(HttpResponse::Ok().finish())
}
} }

@ -2,21 +2,21 @@ use crate::{
apub::{ apub::{
extensions::signatures::verify, extensions::signatures::verify,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
insert_activity,
ActorType, ActorType,
}, },
blocking, blocking,
db::{
activity::insert_activity,
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
Followable,
},
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
LemmyError, LemmyError,
}; };
use activitystreams::activity::Undo; use activitystreams::activity::Undo;
use activitystreams_new::activity::Follow; use activitystreams_new::activity::Follow;
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
Followable,
};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug; use std::fmt::Debug;

@ -1,9 +1,7 @@
use crate::{ use crate::LemmyError;
db::{category::Category, Crud},
LemmyError,
};
use activitystreams::{ext::Extension, Actor}; use activitystreams::{ext::Extension, Actor};
use diesel::PgConnection; use diesel::PgConnection;
use lemmy_db::{category::Category, Crud};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]

@ -9,7 +9,6 @@ use log::debug;
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
pkey::PKey, pkey::PKey,
rsa::Rsa,
sign::{Signer, Verifier}, sign::{Signer, Verifier},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -19,23 +18,6 @@ lazy_static! {
static ref HTTP_SIG_CONFIG: Config = Config::new(); static ref HTTP_SIG_CONFIG: Config = Config::new();
} }
pub struct Keypair {
pub private_key: String,
pub public_key: String,
}
/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn generate_actor_keypair() -> Result<Keypair, LemmyError> {
let rsa = Rsa::generate(2048)?;
let pkey = PKey::from_rsa(rsa)?;
let public_key = pkey.public_key_to_pem()?;
let private_key = pkey.private_key_to_pem_pkcs8()?;
Ok(Keypair {
private_key: String::from_utf8(private_key)?,
public_key: String::from_utf8(public_key)?,
})
}
/// Signs request headers with the given keypair. /// Signs request headers with the given keypair.
pub async fn sign( pub async fn sign(
request: ClientRequest, request: ClientRequest,

@ -1,39 +1,29 @@
use crate::{ use crate::{
api::site::SearchResponse, api::site::SearchResponse,
apub::{ apub::{is_apub_id_valid, FromApub, GroupExt, PageExt, PersonExt, APUB_JSON_CONTENT_TYPE},
get_apub_protocol_string,
is_apub_id_valid,
FromApub,
GroupExt,
PageExt,
PersonExt,
APUB_JSON_CONTENT_TYPE,
},
blocking, blocking,
db::{
comment::{Comment, CommentForm},
comment_view::CommentView,
community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
community_view::CommunityView,
post::{Post, PostForm},
post_view::PostView,
user::{UserForm, User_},
user_view::UserView,
Crud,
Joinable,
SearchType,
},
naive_now,
request::{retry, RecvError}, request::{retry, RecvError},
routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}, routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
DbPool, DbPool, LemmyError,
LemmyError,
}; };
use activitystreams::object::Note; use activitystreams::object::Note;
use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri}; use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri};
use actix_web::client::Client; use actix_web::client::Client;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{result::Error::NotFound, PgConnection}; use diesel::{result::Error::NotFound, PgConnection};
use lemmy_db::{
comment::{Comment, CommentForm},
comment_view::CommentView,
community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
community_view::CommunityView,
naive_now,
post::{Post, PostForm},
post_view::PostView,
user::{UserForm, User_},
user_view::UserView,
Crud, Joinable, SearchType,
};
use lemmy_utils::get_apub_protocol_string;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::{fmt::Debug, time::Duration}; use std::{fmt::Debug, time::Duration};
@ -171,15 +161,15 @@ pub async fn search_by_apub_id(
response response
} }
SearchAcceptedObjects::Page(mut p) => { SearchAcceptedObjects::Page(p) => {
let post_form = PostForm::from_apub(&mut p, client, pool).await?; let post_form = PostForm::from_apub(&p, client, pool).await?;
let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??; let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??]; response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
response response
} }
SearchAcceptedObjects::Comment(mut c) => { SearchAcceptedObjects::Comment(c) => {
let post_url = c let post_url = c
.object_props .object_props
.get_many_in_reply_to_xsd_any_uris() .get_many_in_reply_to_xsd_any_uris()
@ -189,9 +179,9 @@ pub async fn search_by_apub_id(
.to_string(); .to_string();
// TODO: also fetch parent comments if any // TODO: also fetch parent comments if any
let mut post = fetch_remote_object(client, &Url::parse(&post_url)?).await?; let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
let post_form = PostForm::from_apub(&mut post, client, pool).await?; let post_form = PostForm::from_apub(&post, client, pool).await?;
let comment_form = CommentForm::from_apub(&mut c, client, pool).await?; let comment_form = CommentForm::from_apub(&c, client, pool).await?;
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??; blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??; let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
@ -221,9 +211,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
// If its older than a day, re-fetch it // If its older than a day, re-fetch it
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => { Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
debug!("Fetching and updating from remote user: {}", apub_id); debug!("Fetching and updating from remote user: {}", apub_id);
let mut person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?; let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
let mut uf = UserForm::from_apub(&mut person, client, pool).await?; let mut uf = UserForm::from_apub(&person, client, pool).await?;
uf.last_refreshed_at = Some(naive_now()); uf.last_refreshed_at = Some(naive_now());
let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??; let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
@ -232,9 +222,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
Ok(u) => Ok(u), Ok(u) => Ok(u),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id); debug!("Fetching and creating remote user: {}", apub_id);
let mut person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?; let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
let uf = UserForm::from_apub(&mut person, client, pool).await?; let uf = UserForm::from_apub(&person, client, pool).await?;
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??; let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
Ok(user) Ok(user)
@ -272,9 +262,9 @@ pub async fn get_or_fetch_and_upsert_remote_community(
match community { match community {
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => { Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
debug!("Fetching and updating from remote community: {}", apub_id); debug!("Fetching and updating from remote community: {}", apub_id);
let mut group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?; let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
let mut cf = CommunityForm::from_apub(&mut group, client, pool).await?; let mut cf = CommunityForm::from_apub(&group, client, pool).await?;
cf.last_refreshed_at = Some(naive_now()); cf.last_refreshed_at = Some(naive_now());
let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??; let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??;
@ -283,13 +273,13 @@ pub async fn get_or_fetch_and_upsert_remote_community(
Ok(c) => Ok(c), Ok(c) => Ok(c),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id); debug!("Fetching and creating remote community: {}", apub_id);
let mut group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?; let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
let cf = CommunityForm::from_apub(&mut group, client, pool).await?; let cf = CommunityForm::from_apub(&group, client, pool).await?;
let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??; let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??;
// Also add the community moderators too // Also add the community moderators too
let attributed_to = group.inner.take_attributed_to().unwrap(); let attributed_to = group.inner.attributed_to().unwrap();
let creator_and_moderator_uris: Vec<&XsdAnyUri> = attributed_to let creator_and_moderator_uris: Vec<&XsdAnyUri> = attributed_to
.as_many() .as_many()
.unwrap() .unwrap()
@ -349,8 +339,8 @@ pub async fn get_or_fetch_and_insert_remote_post(
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id); debug!("Fetching and creating remote post: {}", post_ap_id);
let mut post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?; let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
let post_form = PostForm::from_apub(&mut post, client, pool).await?; let post_form = PostForm::from_apub(&post, client, pool).await?;
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??; let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
@ -387,8 +377,8 @@ pub async fn get_or_fetch_and_insert_remote_comment(
"Fetching and creating remote comment and its parents: {}", "Fetching and creating remote comment and its parents: {}",
comment_ap_id comment_ap_id
); );
let mut comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?; let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
let comment_form = CommentForm::from_apub(&mut comment, client, pool).await?; let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??; let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;

@ -16,14 +16,10 @@ use crate::{
page_extension::PageExtension, page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension}, signatures::{PublicKey, PublicKeyExtension},
}, },
convert_datetime, blocking,
db::user::User_,
request::{retry, RecvError}, request::{retry, RecvError},
routes::webfinger::WebFingerResponse, routes::webfinger::WebFingerResponse,
DbPool, DbPool, LemmyError,
LemmyError,
MentionData,
Settings,
}; };
use activitystreams::object::Page; use activitystreams::object::Page;
use activitystreams_ext::{Ext1, Ext2}; use activitystreams_ext::{Ext1, Ext2};
@ -35,6 +31,9 @@ use activitystreams_new::{
}; };
use actix_web::{body::Body, client::Client, HttpResponse}; use actix_web::{body::Body, client::Client, HttpResponse};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use failure::_core::fmt::Debug;
use lemmy_db::{activity::do_insert_activity, user::User_};
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData};
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
@ -45,14 +44,6 @@ type PageExt = Ext1<Page, PageExtension>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
pub enum EndpointType {
Community,
User,
Post,
Comment,
PrivateMessage,
}
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers. /// headers.
fn create_apub_response<T>(data: &T) -> HttpResponse<Body> fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
@ -73,34 +64,6 @@ where
.json(data) .json(data)
} }
/// Generates the ActivityPub ID for a given object type and ID.
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::User => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Url::parse(&format!(
"{}://{}/{}/{}",
get_apub_protocol_string(),
Settings::get().hostname,
point,
name
))
.unwrap()
}
pub fn get_apub_protocol_string() -> &'static str {
if Settings::get().federation.tls_enabled {
"https"
} else {
"http"
}
}
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list. // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
fn is_apub_id_valid(apub_id: &Url) -> bool { fn is_apub_id_valid(apub_id: &Url) -> bool {
debug!("Checking {}", apub_id); debug!("Checking {}", apub_id);
@ -165,7 +128,7 @@ fn create_tombstone(
pub trait FromApub { pub trait FromApub {
type ApubType; type ApubType;
async fn from_apub( async fn from_apub(
apub: &mut Self::ApubType, apub: &Self::ApubType,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<Self, LemmyError> ) -> Result<Self, LemmyError>
@ -374,3 +337,19 @@ pub async fn fetch_webfinger_url(
.to_owned() .to_owned()
.ok_or_else(|| format_err!("No href found.").into()) .ok_or_else(|| format_err!("No href found.").into())
} }
pub async fn insert_activity<T>(
user_id: i32,
data: T,
local: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + Debug + Send + 'static,
{
blocking(pool, move |conn| {
do_insert_activity(conn, user_id, &data, local)
})
.await??;
Ok(())
}

@ -1,31 +1,14 @@
use crate::{ use crate::{
apub::{ apub::{
activities::{populate_object_props, send_activity_to_community}, activities::{populate_object_props, send_activity_to_community},
create_apub_response, create_apub_response, create_apub_tombstone_response, create_tombstone,
create_apub_tombstone_response,
create_tombstone,
extensions::page_extension::PageExtension, extensions::page_extension::PageExtension,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
get_apub_protocol_string, ActorType, ApubLikeableType, ApubObjectType, FromApub, PageExt, ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
FromApub,
PageExt,
ToApub,
}, },
blocking, blocking,
convert_datetime,
db::{
community::Community,
post::{Post, PostForm},
user::User_,
Crud,
},
routes::DbPoolParam, routes::DbPoolParam,
DbPool, DbPool, LemmyError,
LemmyError,
Settings,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
@ -36,6 +19,13 @@ use activitystreams::{
use activitystreams_ext::Ext1; use activitystreams_ext::Ext1;
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use lemmy_db::{
community::Community,
post::{Post, PostForm},
user::User_,
Crud,
};
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -164,7 +154,7 @@ impl FromApub for PostForm {
/// Parse an ActivityPub page received from another instance into a Lemmy post. /// Parse an ActivityPub page received from another instance into a Lemmy post.
async fn from_apub( async fn from_apub(
page: &mut PageExt, page: &PageExt,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<PostForm, LemmyError> { ) -> Result<PostForm, LemmyError> {

@ -1,22 +1,9 @@
use crate::{ use crate::{
apub::{ apub::{
activities::send_activity, activities::send_activity, create_tombstone, fetcher::get_or_fetch_and_upsert_remote_user,
create_tombstone, insert_activity, ApubObjectType, FromApub, ToApub,
fetcher::get_or_fetch_and_upsert_remote_user,
ApubObjectType,
FromApub,
ToApub,
}, },
blocking, blocking, DbPool, LemmyError,
convert_datetime,
db::{
activity::insert_activity,
private_message::{PrivateMessage, PrivateMessageForm},
user::User_,
Crud,
},
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Undo, Update}, activity::{Create, Delete, Undo, Update},
@ -25,6 +12,12 @@ use activitystreams::{
}; };
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{
private_message::{PrivateMessage, PrivateMessageForm},
user::User_,
Crud,
};
use lemmy_utils::convert_datetime;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage { impl ToApub for PrivateMessage {
@ -71,7 +64,7 @@ impl FromApub for PrivateMessageForm {
/// Parse an ActivityPub note received from another instance into a Lemmy Private message /// Parse an ActivityPub note received from another instance into a Lemmy Private message
async fn from_apub( async fn from_apub(
note: &mut Note, note: &Note,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<PrivateMessageForm, LemmyError> { ) -> Result<PrivateMessageForm, LemmyError> {

@ -5,47 +5,39 @@ use crate::{
post::PostResponse, post::PostResponse,
}, },
apub::{ apub::{
community::do_announce,
extensions::signatures::verify, extensions::signatures::verify,
fetcher::{ fetcher::{
get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post,
get_or_fetch_and_insert_remote_post, get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user,
get_or_fetch_and_upsert_remote_community,
get_or_fetch_and_upsert_remote_user,
}, },
FromApub, insert_activity, FromApub, GroupExt, PageExt,
GroupExt,
PageExt,
}, },
blocking, blocking,
db::{
activity::insert_activity,
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
comment_view::CommentView,
community::{Community, CommunityForm},
community_view::CommunityView,
post::{Post, PostForm, PostLike, PostLikeForm},
post_view::PostView,
Crud,
Likeable,
},
naive_now,
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
scrape_text_for_mentions,
websocket::{ websocket::{
server::{SendComment, SendCommunityRoomMessage, SendPost}, server::{SendComment, SendCommunityRoomMessage, SendPost},
UserOperation, UserOperation,
}, },
DbPool, DbPool, LemmyError,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Announce, Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{Announce, Create, Delete, Dislike, Like, Remove, Undo, Update},
object::Note, object::Note,
Activity, Activity, Base, BaseBox,
Base,
BaseBox,
}; };
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
comment_view::CommentView,
community::{Community, CommunityForm},
community_view::CommunityView,
naive_now,
post::{Post, PostForm, PostLike, PostLikeForm},
post_view::PostView,
Crud, Likeable,
};
use lemmy_utils::scrape_text_for_mentions;
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
@ -234,7 +226,7 @@ where
if community.local { if community.local {
let sending_user = get_or_fetch_and_upsert_remote_user(sender, client, pool).await?; let sending_user = get_or_fetch_and_upsert_remote_user(sender, client, pool).await?;
Community::do_announce(activity, &community, &sending_user, client, pool).await do_announce(activity, &community, &sending_user, client, pool).await
} else { } else {
Ok(HttpResponse::NotFound().finish()) Ok(HttpResponse::NotFound().finish())
} }
@ -335,7 +327,7 @@ async fn receive_create_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut page = create let page = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -353,7 +345,7 @@ async fn receive_create_post(
insert_activity(user.id, create, false, pool).await?; insert_activity(user.id, create, false, pool).await?;
let post = PostForm::from_apub(&mut page, client, pool).await?; let post = PostForm::from_apub(&page, client, pool).await?;
let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??; let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
@ -381,7 +373,7 @@ async fn receive_create_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = create let note = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -399,7 +391,7 @@ async fn receive_create_comment(
insert_activity(user.id, create, false, pool).await?; insert_activity(user.id, create, false, pool).await?;
let comment = CommentForm::from_apub(&mut note, client, pool).await?; let comment = CommentForm::from_apub(&note, client, pool).await?;
let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??; let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
@ -440,7 +432,7 @@ async fn receive_update_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut page = update let page = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -458,7 +450,7 @@ async fn receive_update_post(
insert_activity(user.id, update, false, pool).await?; insert_activity(user.id, update, false, pool).await?;
let post = PostForm::from_apub(&mut page, client, pool).await?; let post = PostForm::from_apub(&page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?
@ -486,7 +478,7 @@ async fn receive_like_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut page = like let page = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -500,7 +492,7 @@ async fn receive_like_post(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let post = PostForm::from_apub(&mut page, client, pool).await?; let post = PostForm::from_apub(&page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?
@ -537,7 +529,7 @@ async fn receive_dislike_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut page = dislike let page = dislike
.dislike_props .dislike_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -555,7 +547,7 @@ async fn receive_dislike_post(
insert_activity(user.id, dislike, false, pool).await?; insert_activity(user.id, dislike, false, pool).await?;
let post = PostForm::from_apub(&mut page, client, pool).await?; let post = PostForm::from_apub(&page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?
@ -592,7 +584,7 @@ async fn receive_update_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = update let note = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -610,7 +602,7 @@ async fn receive_update_comment(
insert_activity(user.id, update, false, pool).await?; insert_activity(user.id, update, false, pool).await?;
let comment = CommentForm::from_apub(&mut note, client, pool).await?; let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -651,7 +643,7 @@ async fn receive_like_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = like let note = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -665,7 +657,7 @@ async fn receive_like_comment(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let comment = CommentForm::from_apub(&mut note, client, pool).await?; let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -709,7 +701,7 @@ async fn receive_dislike_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = dislike let note = dislike
.dislike_props .dislike_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -727,7 +719,7 @@ async fn receive_dislike_comment(
insert_activity(user.id, dislike, false, pool).await?; insert_activity(user.id, dislike, false, pool).await?;
let comment = CommentForm::from_apub(&mut note, client, pool).await?; let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -777,7 +769,7 @@ async fn receive_delete_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut group = delete let group = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -789,7 +781,7 @@ async fn receive_delete_community(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&mut group, client, pool) let community_actor_id = CommunityForm::from_apub(&group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -854,7 +846,7 @@ async fn receive_remove_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut group = remove let group = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -866,7 +858,7 @@ async fn receive_remove_community(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&mut group, client, pool) let community_actor_id = CommunityForm::from_apub(&group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -931,7 +923,7 @@ async fn receive_delete_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut page = delete let page = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -943,7 +935,7 @@ async fn receive_delete_post(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -997,7 +989,7 @@ async fn receive_remove_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut page = remove let page = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1009,7 +1001,7 @@ async fn receive_remove_post(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -1063,7 +1055,7 @@ async fn receive_delete_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut note = delete let note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1075,7 +1067,7 @@ async fn receive_delete_comment(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1131,7 +1123,7 @@ async fn receive_remove_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut note = remove let note = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1143,7 +1135,7 @@ async fn receive_remove_comment(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1259,7 +1251,7 @@ async fn receive_undo_delete_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut note = delete let note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1271,7 +1263,7 @@ async fn receive_undo_delete_comment(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1327,7 +1319,7 @@ async fn receive_undo_remove_comment(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut note = remove let note = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1339,7 +1331,7 @@ async fn receive_undo_remove_comment(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let comment_ap_id = CommentForm::from_apub(&mut note, client, pool).await?.ap_id; let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
@ -1395,7 +1387,7 @@ async fn receive_undo_delete_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut page = delete let page = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1407,7 +1399,7 @@ async fn receive_undo_delete_post(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -1461,7 +1453,7 @@ async fn receive_undo_remove_post(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut page = remove let page = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1473,7 +1465,7 @@ async fn receive_undo_remove_post(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let post_ap_id = PostForm::from_apub(&mut page, client, pool).await?.ap_id; let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
@ -1527,7 +1519,7 @@ async fn receive_undo_delete_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut group = delete let group = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1539,7 +1531,7 @@ async fn receive_undo_delete_community(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&mut group, client, pool) let community_actor_id = CommunityForm::from_apub(&group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -1604,7 +1596,7 @@ async fn receive_undo_remove_community(
.unwrap() .unwrap()
.to_string(); .to_string();
let mut group = remove let group = remove
.remove_props .remove_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1616,7 +1608,7 @@ async fn receive_undo_remove_community(
insert_activity(mod_.id, remove, false, pool).await?; insert_activity(mod_.id, remove, false, pool).await?;
let community_actor_id = CommunityForm::from_apub(&mut group, client, pool) let community_actor_id = CommunityForm::from_apub(&group, client, pool)
.await? .await?
.actor_id; .actor_id;
@ -1704,7 +1696,7 @@ async fn receive_undo_like_comment(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = like let note = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1718,7 +1710,7 @@ async fn receive_undo_like_comment(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let comment = CommentForm::from_apub(&mut note, client, pool).await?; let comment = CommentForm::from_apub(&note, client, pool).await?;
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
.await? .await?
@ -1758,7 +1750,7 @@ async fn receive_undo_like_post(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut page = like let page = like
.like_props .like_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -1772,7 +1764,7 @@ async fn receive_undo_like_post(
insert_activity(user.id, like, false, pool).await?; insert_activity(user.id, like, false, pool).await?;
let post = PostForm::from_apub(&mut page, client, pool).await?; let post = PostForm::from_apub(&page, client, pool).await?;
let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
.await? .await?

@ -1,15 +1,12 @@
use crate::{ use crate::{
apub::{activities::send_activity, create_apub_response, ActorType, FromApub, PersonExt, ToApub}, api::claims::Claims,
blocking, apub::{
convert_datetime, activities::send_activity, create_apub_response, insert_activity, ActorType, FromApub,
db::{ PersonExt, ToApub,
activity::insert_activity,
user::{UserForm, User_},
}, },
naive_now, blocking,
routes::DbPoolParam, routes::DbPoolParam,
DbPool, DbPool, LemmyError,
LemmyError,
}; };
use activitystreams_ext::Ext1; use activitystreams_ext::Ext1;
use activitystreams_new::{ use activitystreams_new::{
@ -22,6 +19,11 @@ use activitystreams_new::{
}; };
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use failure::_core::str::FromStr; use failure::_core::str::FromStr;
use lemmy_db::{
naive_now,
user::{UserForm, User_},
};
use lemmy_utils::convert_datetime;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -185,8 +187,8 @@ impl ActorType for User_ {
impl FromApub for UserForm { impl FromApub for UserForm {
type ApubType = PersonExt; type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user. /// Parse an ActivityPub person received from another instance into a Lemmy user.
async fn from_apub(person: &mut PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> { async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
let avatar = match person.take_icon() { let avatar = match person.icon() {
Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone()) Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
.unwrap() .unwrap()
.unwrap() .unwrap()
@ -199,19 +201,19 @@ impl FromApub for UserForm {
Ok(UserForm { Ok(UserForm {
name: person name: person
.take_name() .name()
.unwrap() .unwrap()
.as_single_xsd_string() .as_single_xsd_string()
.unwrap() .unwrap()
.into(), .into(),
preferred_username: person.inner.take_preferred_username(), preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
password_encrypted: "".to_string(), password_encrypted: "".to_string(),
admin: false, admin: false,
banned: false, banned: false,
email: None, email: None,
avatar, avatar,
updated: person updated: person
.take_updated() .updated()
.map(|u| u.as_ref().to_owned().naive_local()), .map(|u| u.as_ref().to_owned().naive_local()),
show_nsfw: false, show_nsfw: false,
theme: "".to_string(), theme: "".to_string(),
@ -223,7 +225,7 @@ impl FromApub for UserForm {
matrix_user_id: None, matrix_user_id: None,
actor_id: person.id().unwrap().to_string(), actor_id: person.id().unwrap().to_string(),
bio: person bio: person
.take_summary() .summary()
.map(|s| s.as_single_xsd_string().unwrap().into()), .map(|s| s.as_single_xsd_string().unwrap().into()),
local: false, local: false,
private_key: None, private_key: None,
@ -240,7 +242,7 @@ pub async fn get_apub_user_http(
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name; let user_name = info.into_inner().user_name;
let user = blocking(&db, move |conn| { let user = blocking(&db, move |conn| {
User_::find_by_email_or_username(conn, &user_name) Claims::find_by_email_or_username(conn, &user_name)
}) })
.await??; .await??;
let u = user.to_apub(&db).await?; let u = user.to_apub(&db).await?;

@ -3,29 +3,26 @@ use crate::{
apub::{ apub::{
extensions::signatures::verify, extensions::signatures::verify,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
FromApub, insert_activity, FromApub,
}, },
blocking, blocking,
db::{
activity::insert_activity,
community::{CommunityFollower, CommunityFollowerForm},
private_message::{PrivateMessage, PrivateMessageForm},
private_message_view::PrivateMessageView,
user::User_,
Crud,
Followable,
},
naive_now,
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
websocket::{server::SendUserRoomMessage, UserOperation}, websocket::{server::SendUserRoomMessage, UserOperation},
DbPool, DbPool, LemmyError,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, Create, Delete, Undo, Update}, activity::{Accept, Create, Delete, Undo, Update},
object::Note, object::Note,
}; };
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{
community::{CommunityFollower, CommunityFollowerForm},
naive_now,
private_message::{PrivateMessage, PrivateMessageForm},
private_message_view::PrivateMessageView,
user::User_,
Crud, Followable,
};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug; use std::fmt::Debug;
@ -116,7 +113,7 @@ async fn receive_create_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = create let note = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -135,7 +132,7 @@ async fn receive_create_private_message(
insert_activity(user.id, create, false, pool).await?; insert_activity(user.id, create, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&mut note, client, pool).await?; let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let inserted_private_message = blocking(pool, move |conn| { let inserted_private_message = blocking(pool, move |conn| {
PrivateMessage::create(conn, &private_message) PrivateMessage::create(conn, &private_message)
@ -168,7 +165,7 @@ async fn receive_update_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = update let note = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -187,7 +184,7 @@ async fn receive_update_private_message(
insert_activity(user.id, update, false, pool).await?; insert_activity(user.id, update, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&mut note, client, pool).await?; let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_ap_id = private_message_form.ap_id.clone(); let private_message_ap_id = private_message_form.ap_id.clone();
let private_message = blocking(pool, move |conn| { let private_message = blocking(pool, move |conn| {
@ -228,7 +225,7 @@ async fn receive_delete_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let mut note = delete let note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -247,7 +244,7 @@ async fn receive_delete_private_message(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&mut note, client, pool).await?; let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_ap_id = private_message_form.ap_id; let private_message_ap_id = private_message_form.ap_id;
let private_message = blocking(pool, move |conn| { let private_message = blocking(pool, move |conn| {
@ -308,7 +305,7 @@ async fn receive_undo_delete_private_message(
.to_owned() .to_owned()
.into_concrete::<Delete>()?; .into_concrete::<Delete>()?;
let mut note = delete let note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
.to_owned() .to_owned()
@ -327,7 +324,7 @@ async fn receive_undo_delete_private_message(
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&mut note, client, pool).await?; let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_ap_id = private_message.ap_id.clone(); let private_message_ap_id = private_message.ap_id.clone();
let private_message_id = blocking(pool, move |conn| { let private_message_id = blocking(pool, move |conn| {

@ -1,18 +1,16 @@
// This is for db migrations that require code // This is for db migrations that require code
use super::{ use crate::LemmyError;
use diesel::*;
use lemmy_db::{
comment::Comment, comment::Comment,
community::{Community, CommunityForm}, community::{Community, CommunityForm},
naive_now,
post::Post, post::Post,
private_message::PrivateMessage, private_message::PrivateMessage,
user::{UserForm, User_}, user::{UserForm, User_},
Crud,
}; };
use crate::{ use lemmy_utils::{generate_actor_keypair, make_apub_endpoint, EndpointType};
apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
db::Crud,
naive_now,
LemmyError,
};
use diesel::*;
use log::info; use log::info;
pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> { pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
@ -26,7 +24,7 @@ pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> { fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::user_::dsl::*; use lemmy_db::schema::user_::dsl::*;
info!("Running user_updates_2020_04_02"); info!("Running user_updates_2020_04_02");
@ -77,7 +75,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> { fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::community::dsl::*; use lemmy_db::schema::community::dsl::*;
info!("Running community_updates_2020_04_02"); info!("Running community_updates_2020_04_02");
@ -121,7 +119,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> { fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::post::dsl::*; use lemmy_db::schema::post::dsl::*;
info!("Running post_updates_2020_04_03"); info!("Running post_updates_2020_04_03");
@ -134,7 +132,8 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
sql_query("alter table post disable trigger refresh_post").execute(conn)?; sql_query("alter table post disable trigger refresh_post").execute(conn)?;
for cpost in &incorrect_posts { for cpost in &incorrect_posts {
Post::update_ap_id(&conn, cpost.id)?; let apub_id = make_apub_endpoint(EndpointType::Post, &cpost.id.to_string()).to_string();
Post::update_ap_id(&conn, cpost.id, apub_id)?;
} }
info!("{} post rows updated.", incorrect_posts.len()); info!("{} post rows updated.", incorrect_posts.len());
@ -145,7 +144,7 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> { fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::comment::dsl::*; use lemmy_db::schema::comment::dsl::*;
info!("Running comment_updates_2020_04_03"); info!("Running comment_updates_2020_04_03");
@ -158,7 +157,8 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
sql_query("alter table comment disable trigger refresh_comment").execute(conn)?; sql_query("alter table comment disable trigger refresh_comment").execute(conn)?;
for ccomment in &incorrect_comments { for ccomment in &incorrect_comments {
Comment::update_ap_id(&conn, ccomment.id)?; let apub_id = make_apub_endpoint(EndpointType::Comment, &ccomment.id.to_string()).to_string();
Comment::update_ap_id(&conn, ccomment.id, apub_id)?;
} }
sql_query("alter table comment enable trigger refresh_comment").execute(conn)?; sql_query("alter table comment enable trigger refresh_comment").execute(conn)?;
@ -169,7 +169,7 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
} }
fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> { fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::private_message::dsl::*; use lemmy_db::schema::private_message::dsl::*;
info!("Running private_message_updates_2020_05_05"); info!("Running private_message_updates_2020_05_05");
@ -180,7 +180,8 @@ fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyEr
.load::<PrivateMessage>(conn)?; .load::<PrivateMessage>(conn)?;
for cpm in &incorrect_pms { for cpm in &incorrect_pms {
PrivateMessage::update_ap_id(&conn, cpm.id)?; let apub_id = make_apub_endpoint(EndpointType::PrivateMessage, &cpm.id.to_string()).to_string();
PrivateMessage::update_ap_id(&conn, cpm.id, apub_id)?;
} }
info!("{} private message rows updated.", incorrect_pms.len()); info!("{} private message rows updated.", incorrect_pms.len());

@ -5,76 +5,34 @@ pub extern crate strum_macros;
pub extern crate lazy_static; pub extern crate lazy_static;
#[macro_use] #[macro_use]
pub extern crate failure; pub extern crate failure;
#[macro_use]
pub extern crate diesel;
pub extern crate actix; pub extern crate actix;
pub extern crate actix_web; pub extern crate actix_web;
pub extern crate bcrypt; pub extern crate bcrypt;
pub extern crate chrono; pub extern crate chrono;
pub extern crate comrak; pub extern crate diesel;
pub extern crate dotenv; pub extern crate dotenv;
pub extern crate jsonwebtoken; pub extern crate jsonwebtoken;
pub extern crate lettre;
pub extern crate lettre_email;
extern crate log; extern crate log;
pub extern crate openssl; pub extern crate openssl;
pub extern crate rand;
pub extern crate regex;
pub extern crate rss; pub extern crate rss;
pub extern crate serde; pub extern crate serde;
pub extern crate serde_json; pub extern crate serde_json;
pub extern crate sha2; pub extern crate sha2;
pub extern crate strum; pub extern crate strum;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
where
F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
T: Send + 'static,
{
let pool = pool.clone();
let res = actix_web::web::block(move || {
let conn = pool.get()?;
let res = (f)(&conn);
Ok(res) as Result<_, LemmyError>
})
.await?;
Ok(res)
}
pub mod api; pub mod api;
pub mod apub; pub mod apub;
pub mod db; pub mod code_migrations;
pub mod rate_limit; pub mod rate_limit;
pub mod request; pub mod request;
pub mod routes; pub mod routes;
pub mod schema;
pub mod settings;
pub mod version; pub mod version;
pub mod websocket; pub mod websocket;
use crate::{ use crate::request::{retry, RecvError};
request::{retry, RecvError},
settings::Settings,
};
use actix_web::{client::Client, dev::ConnectionInfo}; use actix_web::{client::Client, dev::ConnectionInfo};
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use itertools::Itertools;
use lettre::{
smtp::{
authentication::{Credentials, Mechanism},
extension::ClientId,
ConnectionReuseParameters,
},
ClientSecurity,
SmtpClient,
Transport,
};
use lettre_email::Email;
use log::error; use log::error;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Regex, RegexBuilder};
use serde::Deserialize; use serde::Deserialize;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>; pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
@ -89,14 +47,6 @@ pub struct LemmyError {
inner: failure::Error, inner: failure::Error,
} }
impl std::fmt::Display for LemmyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl actix_web::error::ResponseError for LemmyError {}
impl<T> From<T> for LemmyError impl<T> From<T> for LemmyError
where where
T: Into<failure::Error>, T: Into<failure::Error>,
@ -106,113 +56,13 @@ where
} }
} }
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> { impl std::fmt::Display for LemmyError {
DateTime::<Utc>::from_utc(ndt, Utc) fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
} self.inner.fmt(f)
pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc()
}
pub fn naive_from_unix(time: i64) -> NaiveDateTime {
NaiveDateTime::from_timestamp(time, 0)
}
pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
let now = Local::now();
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
let response = retry(|| client.get(test).send()).await?;
if response
.headers()
.get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))?
.to_str()?
.starts_with("image/")
{
Ok(())
} else {
Err(format_err!("Not an image type.").into())
}
}
pub fn remove_slurs(test: &str) -> String {
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
pub fn slur_check(test: &str) -> Result<(), Vec<&str>> {
let mut matches: Vec<&str> = SLUR_REGEX.find_iter(test).map(|mat| mat.as_str()).collect();
// Unique
matches.sort_unstable();
matches.dedup();
if matches.is_empty() {
Ok(())
} else {
Err(matches)
} }
} }
pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String { impl actix_web::error::ResponseError for LemmyError {}
let start = "No slurs - ";
let combined = &slurs.join(", ");
[start, combined].concat()
}
pub fn generate_random_string() -> String {
thread_rng().sample_iter(&Alphanumeric).take(30).collect()
}
pub fn send_email(
subject: &str,
to_email: &str,
to_username: &str,
html: &str,
) -> Result<(), String> {
let email_config = Settings::get().email.ok_or("no_email_setup")?;
let email = Email::builder()
.to((to_email, to_username))
.from(email_config.smtp_from_address.to_owned())
.subject(subject)
.html(html)
.build()
.unwrap();
let mailer = if email_config.use_tls {
SmtpClient::new_simple(&email_config.smtp_server).unwrap()
} else {
SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
}
.hello_name(ClientId::Domain(Settings::get().hostname))
.smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited);
let mailer = if let (Some(login), Some(password)) =
(&email_config.smtp_login, &email_config.smtp_password)
{
mailer.credentials(Credentials::new(login.to_owned(), password.to_owned()))
} else {
mailer
};
let mut transport = mailer.transport();
let result = transport.send(email.into());
transport.close();
match result {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct IframelyResponse { pub struct IframelyResponse {
@ -319,8 +169,20 @@ async fn fetch_iframely_and_pictrs_data(
} }
} }
pub fn markdown_to_html(text: &str) -> String { pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
comrak::markdown_to_html(text, &comrak::ComrakOptions::default()) let response = retry(|| client.get(test).send()).await?;
if response
.headers()
.get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))?
.to_str()?
.starts_with("image/")
{
Ok(())
} else {
Err(format_err!("Not an image type.").into())
}
} }
pub fn get_ip(conn_info: &ConnectionInfo) -> String { pub fn get_ip(conn_info: &ConnectionInfo) -> String {
@ -333,127 +195,37 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> String {
.to_string() .to_string()
} }
// TODO nothing is done with community / group webfingers yet, so just ignore those for now pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
#[derive(Clone, PartialEq, Eq, Hash)] where
pub struct MentionData { F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
pub name: String, T: Send + 'static,
pub domain: String, {
} let pool = pool.clone();
let res = actix_web::web::block(move || {
impl MentionData { let conn = pool.get()?;
pub fn is_local(&self) -> bool { let res = (f)(&conn);
Settings::get().hostname.eq(&self.domain) Ok(res) as Result<_, LemmyError>
} })
pub fn full_name(&self) -> String { .await?;
format!("@{}@{}", &self.name, &self.domain)
}
}
pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
let mut out: Vec<MentionData> = Vec::new();
for caps in WEBFINGER_USER_REGEX.captures_iter(text) {
out.push(MentionData {
name: caps["name"].to_string(),
domain: caps["domain"].to_string(),
});
}
out.into_iter().unique().collect()
}
pub fn is_valid_username(name: &str) -> bool {
VALID_USERNAME_REGEX.is_match(name)
}
pub fn is_valid_community_name(name: &str) -> bool { Ok(res)
VALID_COMMUNITY_NAME_REGEX.is_match(name)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::is_image_content_type;
is_email_regex,
is_image_content_type,
is_valid_community_name,
is_valid_username,
remove_slurs,
scrape_text_for_mentions,
slur_check,
slurs_vec_to_str,
};
#[test]
fn test_mentions_regex() {
let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)";
let mentions = scrape_text_for_mentions(text);
assert_eq!(mentions[0].name, "tedu".to_string());
assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
}
#[test] #[test]
fn test_image() { fn test_image() {
actix_rt::System::new("tset_image").block_on(async move { actix_rt::System::new("tset_image").block_on(async move {
let client = actix_web::client::Client::default(); let client = actix_web::client::Client::default();
assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok()); assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
assert!(is_image_content_type(&client, assert!(is_image_content_type(&client,
"https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20" "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
) )
.await.is_err() .await.is_err()
); );
}); });
}
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test]
fn test_valid_register_username() {
assert!(is_valid_username("Hello_98"));
assert!(is_valid_username("ten"));
assert!(!is_valid_username("Hello-98"));
assert!(!is_valid_username("a"));
assert!(!is_valid_username(""));
}
#[test]
fn test_valid_community_name() {
assert!(is_valid_community_name("example"));
assert!(is_valid_community_name("example_community"));
assert!(!is_valid_community_name("Example"));
assert!(!is_valid_community_name("Ex"));
assert!(!is_valid_community_name(""));
}
#[test]
fn test_slur_filter() {
let test =
"coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
let slur_free = "No slurs here";
assert_eq!(
remove_slurs(&test),
"*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
.to_string()
);
let has_slurs_vec = vec![
"Niggerz",
"coons",
"dindu",
"ladyboy",
"retardeds",
"tranny",
];
let has_slurs_err_str = "No slurs - Niggerz, coons, dindu, ladyboy, retardeds, tranny";
assert_eq!(slur_check(test), Err(has_slurs_vec));
assert_eq!(slur_check(slur_free), Ok(()));
if let Err(slur_vec) = slur_check(test) {
assert_eq!(&slurs_vec_to_str(slur_vec), has_slurs_err_str);
}
} }
// These helped with testing // These helped with testing
@ -470,21 +242,4 @@ mod tests {
// let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu"); // let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
// assert!(res_other.is_err()); // assert!(res_other.is_err());
// } // }
// #[test]
// fn test_send_email() {
// let result = send_email("not a subject", "test_email@gmail.com", "ur user", "<h1>HI there</h1>");
// assert!(result.is_ok());
// }
}
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 = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bn(i|1)g(\b|g?(a|er)?(s|z)?)\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)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
// TODO keep this old one, it didn't work with port well tho
// static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
} }

@ -22,22 +22,20 @@ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,
}; };
use lemmy_db::get_database_url_from_env;
use lemmy_server::{ use lemmy_server::{
blocking, blocking,
db::code_migrations::run_advanced_migrations, code_migrations::run_advanced_migrations,
rate_limit::{rate_limiter::RateLimiter, RateLimit}, rate_limit::{rate_limiter::RateLimiter, RateLimit},
routes::{api, federation, feeds, index, nodeinfo, webfinger}, routes::{api, federation, feeds, index, nodeinfo, webfinger},
settings::Settings,
websocket::server::*, websocket::server::*,
LemmyError, LemmyError,
}; };
use regex::Regex; use lemmy_utils::{settings::Settings, CACHE_CONTROL_REGEX};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
lazy_static! { lazy_static! {
static ref CACHE_CONTROL_REGEX: Regex =
Regex::new("^((text|image)/.+|application/javascript)$").unwrap();
// static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 365 * 24 * 60 * 60); // static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 365 * 24 * 60 * 60);
// Test out 1 hour here, this is breaking some things // Test out 1 hour here, this is breaking some things
static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 60 * 60); static ref CACHE_CONTROL_VALUE: String = format!("public, max-age={}", 60 * 60);
@ -51,11 +49,15 @@ async fn main() -> Result<(), LemmyError> {
let settings = Settings::get(); let settings = Settings::get();
// Set up the r2d2 connection pool // Set up the r2d2 connection pool
let manager = ConnectionManager::<PgConnection>::new(&settings.get_database_url()); let db_url = match get_database_url_from_env() {
Ok(url) => url,
Err(_) => settings.get_database_url(),
};
let manager = ConnectionManager::<PgConnection>::new(&db_url);
let pool = Pool::builder() let pool = Pool::builder()
.max_size(settings.database.pool_size) .max_size(settings.database.pool_size)
.build(manager) .build(manager)
.unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url())); .unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
// Run the migrations from code // Run the migrations from code
blocking(&pool, move |conn| { blocking(&pool, move |conn| {

@ -1,7 +1,8 @@
use super::{IPAddr, Settings}; use super::IPAddr;
use crate::{get_ip, settings::RateLimitConfig, LemmyError}; use crate::{get_ip, LemmyError};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use futures::future::{ok, Ready}; use futures::future::{ok, Ready};
use lemmy_utils::settings::{RateLimitConfig, Settings};
use rate_limiter::{RateLimitType, RateLimiter}; use rate_limiter::{RateLimitType, RateLimiter};
use std::{ use std::{
future::Future, future::Future,

@ -1,21 +1,23 @@
use crate::{ use crate::apub::{
apub::{ comment::get_apub_comment,
comment::get_apub_comment, community::*,
community::*, community_inbox::community_inbox,
community_inbox::community_inbox, post::get_apub_post,
post::get_apub_post, shared_inbox::shared_inbox,
shared_inbox::shared_inbox, user::*,
user::*, user_inbox::user_inbox,
user_inbox::user_inbox, APUB_JSON_CONTENT_TYPE,
APUB_JSON_CONTENT_TYPE,
},
settings::Settings,
}; };
use actix_web::*; use actix_web::*;
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
use lemmy_utils::settings::Settings;
use sha2::{Digest, Sha256};
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation.enabled { if Settings::get().federation.enabled {
println!("federation enabled, host is {}", Settings::get().hostname); println!("federation enabled, host is {}", Settings::get().hostname);
let digest_verifier = VerifyDigest::new(Sha256::new());
cfg cfg
.service( .service(
web::scope("/") web::scope("/")
@ -38,8 +40,20 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/comment/{comment_id}", web::get().to(get_apub_comment)), .route("/comment/{comment_id}", web::get().to(get_apub_comment)),
) )
// Inboxes dont work with the header guard for some reason. // Inboxes dont work with the header guard for some reason.
.route("/c/{community_name}/inbox", web::post().to(community_inbox)) .service(
.route("/u/{user_name}/inbox", web::post().to(user_inbox)) web::resource("/c/{community_name}/inbox")
.route("/inbox", web::post().to(shared_inbox)); .wrap(digest_verifier.clone())
.route(web::post().to(community_inbox)),
)
.service(
web::resource("/u/{user_name}/inbox")
.wrap(digest_verifier.clone())
.route(web::post().to(user_inbox)),
)
.service(
web::resource("/inbox")
.wrap(digest_verifier)
.route(web::post().to(shared_inbox)),
);
} }
} }

@ -1,26 +1,21 @@
use crate::{ use crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError};
blocking,
db::{
comment_view::{ReplyQueryBuilder, ReplyView},
community::Community,
post_view::{PostQueryBuilder, PostView},
site_view::SiteView,
user::{Claims, User_},
user_mention_view::{UserMentionQueryBuilder, UserMentionView},
ListingType,
SortType,
},
markdown_to_html,
routes::DbPoolParam,
settings::Settings,
LemmyError,
};
use actix_web::{error::ErrorBadRequest, *}; use actix_web::{error::ErrorBadRequest, *};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use diesel::{ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,
}; };
use lemmy_db::{
comment_view::{ReplyQueryBuilder, ReplyView},
community::Community,
post_view::{PostQueryBuilder, PostView},
site_view::SiteView,
user::User_,
user_mention_view::{UserMentionQueryBuilder, UserMentionView},
ListingType,
SortType,
};
use lemmy_utils::{markdown_to_html, settings::Settings};
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder}; use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
use serde::Deserialize; use serde::Deserialize;
use std::str::FromStr; use std::str::FromStr;
@ -131,7 +126,7 @@ fn get_feed_user(
) -> Result<ChannelBuilder, LemmyError> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user = User_::find_by_username(&conn, &user_name)?; let user = User_::find_by_username(&conn, &user_name)?;
let user_url = user.get_profile_url(); let user_url = user.get_profile_url(&Settings::get().hostname);
let posts = PostQueryBuilder::create(&conn) let posts = PostQueryBuilder::create(&conn)
.listing_type(ListingType::All) .listing_type(ListingType::All)

@ -1,6 +1,6 @@
use crate::settings::Settings;
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_web::*; use actix_web::*;
use lemmy_utils::settings::Settings;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg cfg

@ -1,13 +1,7 @@
use crate::{ use crate::{blocking, routes::DbPoolParam, version, LemmyError};
apub::get_apub_protocol_string,
blocking,
db::site_view::SiteView,
routes::DbPoolParam,
version,
LemmyError,
Settings,
};
use actix_web::{body::Body, error::ErrorBadRequest, *}; use actix_web::{body::Body, error::ErrorBadRequest, *};
use lemmy_db::site_view::SiteView;
use lemmy_utils::{get_apub_protocol_string, settings::Settings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;

@ -1,12 +1,7 @@
use crate::{ use crate::{blocking, routes::DbPoolParam, LemmyError};
blocking,
db::{community::Community, user::User_},
routes::DbPoolParam,
LemmyError,
Settings,
};
use actix_web::{error::ErrorBadRequest, web::Query, *}; use actix_web::{error::ErrorBadRequest, web::Query, *};
use regex::Regex; use lemmy_db::{community::Community, user::User_};
use lemmy_utils::{settings::Settings, WEBFINGER_COMMUNITY_REGEX, WEBFINGER_USER_REGEX};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Deserialize)] #[derive(Deserialize)]
@ -40,19 +35,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
} }
} }
lazy_static! {
static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
"^group:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
"^acct:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
}
/// Responds to webfinger requests of the following format. There isn't any real documentation for /// Responds to webfinger requests of the following format. There isn't any real documentation for
/// this, but it described in this blog post: /// this, but it described in this blog post:
/// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social /// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social

@ -1 +1 @@
pub const VERSION: &str = "v0.7.13"; pub const VERSION: &str = "v0.7.19";

5
server/test.sh vendored

@ -0,0 +1,5 @@
#!/bin/sh
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
diesel migration run
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
RUST_TEST_THREADS=1 cargo test --workspace

File diff suppressed because one or more lines are too long

@ -264,3 +264,10 @@ pre {
width: 0px !important; width: 0px !important;
padding: 0 !important; padding: 0 !important;
} }
br.big {
display: block;
content: "";
margin-top: 1rem;
}

File diff suppressed because one or more lines are too long

8
ui/fuse.js vendored

@ -6,12 +6,10 @@ const {
WebIndexPlugin, WebIndexPlugin,
QuantumPlugin, QuantumPlugin,
} = require('fuse-box'); } = require('fuse-box');
// const transformInferno = require('../../dist').default
const transformInferno = require('ts-transform-inferno').default; const transformInferno = require('ts-transform-inferno').default;
const transformClasscat = require('ts-transform-classcat').default; const transformClasscat = require('ts-transform-classcat').default;
let fuse, app; let fuse, app;
let isProduction = false; let isProduction = false;
// var setVersion = require('./set_version.js').setVersion;
Sparky.task('config', _ => { Sparky.task('config', _ => {
fuse = new FuseBox({ fuse = new FuseBox({
@ -45,18 +43,18 @@ Sparky.task('config', _ => {
}); });
app = fuse.bundle('app').instructions('>index.tsx'); app = fuse.bundle('app').instructions('>index.tsx');
}); });
// Sparky.task('version', _ => setVersion());
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
Sparky.task('env', _ => (isProduction = true)); Sparky.task('env', _ => (isProduction = true));
Sparky.task('copy-assets', () => Sparky.task('copy-assets', () =>
Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static') Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
); );
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => { Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
fuse.dev(); fuse.dev({
fallback: 'index.html',
});
app.hmr().watch(); app.hmr().watch();
return fuse.run(); return fuse.run();
}); });
Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => { Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => {
// fuse.dev({ reload: true }); // remove after demo
return fuse.run(); return fuse.run();
}); });

6
ui/package.json vendored

@ -15,7 +15,6 @@
}, },
"keywords": [], "keywords": [],
"dependencies": { "dependencies": {
"@joeattardi/emoji-button": "^2.12.1",
"@types/autosize": "^3.0.6", "@types/autosize": "^3.0.6",
"@types/js-cookie": "^2.2.6", "@types/js-cookie": "^2.2.6",
"@types/jwt-decode": "^2.2.1", "@types/jwt-decode": "^2.2.1",
@ -24,6 +23,7 @@
"@types/node": "^13.11.1", "@types/node": "^13.11.1",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"bootswatch": "^4.3.1", "bootswatch": "^4.3.1",
"choices.js": "^9.0.1",
"classcat": "^4.0.2", "classcat": "^4.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"emoji-short-name": "^1.0.0", "emoji-short-name": "^1.0.0",
@ -37,7 +37,6 @@
"markdown-it": "^10.0.0", "markdown-it": "^10.0.0",
"markdown-it-container": "^2.0.0", "markdown-it-container": "^2.0.0",
"markdown-it-emoji": "^1.4.0", "markdown-it-emoji": "^1.4.0",
"mobius1-selectr": "^2.4.13",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"prettier": "^2.0.4", "prettier": "^2.0.4",
@ -47,7 +46,6 @@
"tippy.js": "^6.1.1", "tippy.js": "^6.1.1",
"toastify-js": "^1.7.0", "toastify-js": "^1.7.0",
"tributejs": "^5.1.3", "tributejs": "^5.1.3",
"twemoji": "^12.1.2",
"ws": "^7.2.3" "ws": "^7.2.3"
}, },
"devDependencies": { "devDependencies": {
@ -72,7 +70,7 @@
"engineStrict": true, "engineStrict": true,
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged" "pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && lint-staged"
} }
}, },
"lint-staged": { "lint-staged": {

@ -0,0 +1,25 @@
import { Component } from 'inferno';
import { i18n } from '../i18next';
interface CakeDayProps {
creatorName: string;
}
export class CakeDay extends Component<CakeDayProps, any> {
render() {
return (
<div
className={`mx-2 d-inline-block unselectable pointer`}
data-tippy-content={this.cakeDayTippy()}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-cake"></use>
</svg>
</div>
);
}
cakeDayTippy(): string {
return i18n.t('cake_day_info', { creator_name: this.props.creatorName });
}
}

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
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 { Prompt } from 'inferno-router'; import { Prompt } from 'inferno-router';
@ -17,7 +18,6 @@ import {
toast, toast,
setupTribute, setupTribute,
wsJsonToRes, wsJsonToRes,
emojiPicker,
pictrsDeleteToast, pictrsDeleteToast,
} from '../utils'; } from '../utils';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
@ -25,6 +25,7 @@ import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name'; import emojiShortName from 'emoji-short-name';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface CommentFormProps { interface CommentFormProps {
postId?: number; postId?: number;
@ -32,6 +33,7 @@ interface CommentFormProps {
onReplyCancel?(): any; onReplyCancel?(): any;
edit?: boolean; edit?: boolean;
disabled?: boolean; disabled?: boolean;
focus?: boolean;
} }
interface CommentFormState { interface CommentFormState {
@ -72,7 +74,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
super(props, context); super(props, context);
this.tribute = setupTribute(); this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState; this.state = this.emptyState;
@ -98,14 +99,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
} }
componentDidMount() { componentDidMount() {
var textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
autosize(textarea); if (textarea) {
this.tribute.attach(textarea); autosize(textarea);
textarea.addEventListener('tribute-replaced', () => { this.tribute.attach(textarea);
this.state.commentForm.content = textarea.value; textarea.addEventListener('tribute-replaced', () => {
this.setState(this.state); this.state.commentForm.content = textarea.value;
autosize.update(textarea); this.setState(this.state);
}); autosize.update(textarea);
});
// Quoting of selected text
let selectedText = window.getSelection().toString();
if (selectedText) {
let quotedText =
selectedText
.split('\n')
.map(t => `> ${t}`)
.join('\n') + '\n\n';
this.state.commentForm.content = quotedText;
this.setState(this.state);
// Not sure why this needs a delay
setTimeout(() => autosize.update(textarea), 10);
}
if (this.props.focus) {
textarea.focus();
}
}
} }
componentDidUpdate() { componentDidUpdate() {
@ -128,133 +149,123 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
when={this.state.commentForm.content} when={this.state.commentForm.content}
message={i18n.t('block_leaving')} message={i18n.t('block_leaving')}
/> />
<form {UserService.Instance.user ? (
id={this.formId} <form
onSubmit={linkEvent(this, this.handleCommentSubmit)} id={this.formId}
> onSubmit={linkEvent(this, this.handleCommentSubmit)}
<div class="form-group row"> >
<div className={`col-sm-12`}> <div class="form-group row">
<textarea <div className={`col-sm-12`}>
id={this.id} <textarea
className={`form-control ${this.state.previewMode && 'd-none'}`} id={this.id}
value={this.state.commentForm.content} className={`form-control ${
onInput={linkEvent(this, this.handleCommentContentChange)} this.state.previewMode && 'd-none'
onPaste={linkEvent(this, this.handleImageUploadPaste)} }`}
required value={this.state.commentForm.content}
disabled={this.props.disabled} onInput={linkEvent(this, this.handleCommentContentChange)}
rows={2} onPaste={linkEvent(this, this.handleImageUploadPaste)}
maxLength={10000} required
/> disabled={this.props.disabled}
{this.state.previewMode && ( rows={2}
<div maxLength={10000}
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.commentForm.content
)}
/> />
)} {this.state.previewMode && (
</div> <div
</div> className="card card-body md-div"
<div class="row"> dangerouslySetInnerHTML={mdToHtml(
<div class="col-sm-12"> this.state.commentForm.content
<button )}
type="submit" />
class="btn btn-sm btn-secondary mr-2"
disabled={this.props.disabled || this.state.loading}
>
{this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<span>{this.state.buttonTitle}</span>
)} )}
</button> </div>
{this.state.commentForm.content && ( </div>
<button <div class="row">
className={`btn btn-sm mr-2 btn-secondary ${ <div class="col-sm-12">
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{this.props.node && (
<button <button
type="button" type="submit"
class="btn btn-sm btn-secondary mr-2" class="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)} disabled={this.props.disabled || this.state.loading}
> >
{i18n.t('cancel')} {this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<span>{this.state.buttonTitle}</span>
)}
</button> </button>
)} {this.state.commentForm.content && (
<a <button
href={markdownHelpUrl} className={`btn btn-sm mr-2 btn-secondary ${
target="_blank" this.state.previewMode && 'active'
class="d-inline-block float-right text-muted font-weight-bold" }`}
title={i18n.t('formatting_help')} onClick={linkEvent(this, this.handlePreviewToggle)}
rel="noopener" >
> {i18n.t('preview')}
<svg class="icon icon-inline"> </button>
<use xlinkHref="#icon-help-circle"></use> )}
</svg> {this.props.node && (
</a> <button
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold"> type="button"
<label class="btn btn-sm btn-secondary mr-2"
htmlFor={`file-upload-${this.id}`} onClick={linkEvent(this, this.handleReplyCancel)}
className={`${UserService.Instance.user && 'pointer'}`} >
data-tippy-content={i18n.t('upload_image')} {i18n.t('cancel')}
</button>
)}
<a
href={markdownHelpUrl}
target="_blank"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
rel="noopener"
> >
<svg class="icon icon-inline"> <svg class="icon icon-inline">
<use xlinkHref="#icon-image"></use> <use xlinkHref="#icon-help-circle"></use>
</svg> </svg>
</label> </a>
<input <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
id={`file-upload-${this.id}`} <label
type="file" htmlFor={`file-upload-${this.id}`}
accept="image/*,video/*" className={`${UserService.Instance.user && 'pointer'}`}
name="file" data-tippy-content={i18n.t('upload_image')}
class="d-none" >
disabled={!UserService.Instance.user} <svg class="icon icon-inline">
onChange={linkEvent(this, this.handleImageUpload)} <use xlinkHref="#icon-image"></use>
/> </svg>
</form> </label>
{this.state.imageLoading && ( <input
<svg class="icon icon-spinner spin"> id={`file-upload-${this.id}`}
<use xlinkHref="#icon-spinner"></use> type="file"
</svg> accept="image/*,video/*"
)} name="file"
<span class="d-none"
onClick={linkEvent(this, this.handleEmojiPickerClick)} disabled={!UserService.Instance.user}
class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold" onChange={linkEvent(this, this.handleImageUpload)}
data-tippy-content={i18n.t('emoji_picker')} />
> </form>
<svg class="icon icon-inline"> {this.state.imageLoading && (
<use xlinkHref="#icon-smile"></use> <svg class="icon icon-spinner spin">
</svg> <use xlinkHref="#icon-spinner"></use>
</span> </svg>
)}
</div>
</div> </div>
</form>
) : (
<div class="alert alert-warning" role="alert">
<svg class="icon icon-inline mr-2">
<use xlinkHref="#icon-alert-triangle"></use>
</svg>
<T i18nKey="must_login" class="d-inline">
#<Link to="/login">#</Link>
</T>
</div> </div>
</form> )}
</div> </div>
); );
} }
setupEmojiPicker() {
emojiPicker.on('emoji', twemojiHtmlStr => {
if (this.state.commentForm.content == null) {
this.state.commentForm.content = '';
}
var el = document.createElement('div');
el.innerHTML = twemojiHtmlStr;
let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
let shortName = `:${emojiShortName[nativeUnicode]}:`;
this.state.commentForm.content += shortName;
this.setState(this.state);
});
}
handleFinished(op: UserOperation, data: CommentResponse) { handleFinished(op: UserOperation, data: CommentResponse) {
let isReply = let isReply =
this.props.node !== undefined && data.comment.parent_id !== null; this.props.node !== undefined && data.comment.parent_id !== null;
@ -302,10 +313,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state); i.setState(i.state);
} }
handleEmojiPickerClick(_i: CommentForm, event: any) {
emojiPicker.togglePicker(event.target);
}
handleCommentContentChange(i: CommentForm, event: any) { handleCommentContentChange(i: CommentForm, event: any) {
i.state.commentForm.content = event.target.value; i.state.commentForm.content = event.target.value;
i.setState(i.state); i.setState(i.state);

@ -32,6 +32,7 @@ import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form'; import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
interface CommentNodeState { interface CommentNodeState {
@ -158,9 +159,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id: node.comment.creator_id, id: node.comment.creator_id,
local: node.comment.creator_local, local: node.comment.creator_local,
actor_id: node.comment.creator_actor_id, actor_id: node.comment.creator_actor_id,
published: node.comment.creator_published,
}} }}
/> />
</span> </span>
{this.isMod && ( {this.isMod && (
<div className="badge badge-light d-none d-sm-inline mr-2"> <div className="badge badge-light d-none d-sm-inline mr-2">
{i18n.t('mod')} {i18n.t('mod')}
@ -184,13 +187,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.props.showCommunity && ( {this.props.showCommunity && (
<> <>
<span class="mx-1">{i18n.t('to')}</span> <span class="mx-1">{i18n.t('to')}</span>
<Link class="mr-2" to={`/c/${node.comment.community_name}`}> <CommunityLink
{node.comment.community_name} community={{
name: node.comment.community_name,
id: node.comment.community_id,
local: node.comment.community_local,
actor_id: node.comment.community_actor_id,
}}
/>
<span class="mx-2"></span>
<Link class="mr-2" to={`/post/${node.comment.post_id}`}>
{node.comment.post_name}
</Link> </Link>
</> </>
)} )}
<div <button
className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" class="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)} onClick={linkEvent(this, this.handleCommentCollapse)}
> >
{this.state.collapsed ? ( {this.state.collapsed ? (
@ -202,9 +214,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<use xlinkHref="#icon-minus-square"></use> <use xlinkHref="#icon-minus-square"></use>
</svg> </svg>
)} )}
</div> </button>
<span {/* This is an expanding spacer for mobile */}
className={`unselectable pointer ${this.scoreColor}`} <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
<button
className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
onClick={linkEvent(node, this.handleCommentUpvote)} onClick={linkEvent(node, this.handleCommentUpvote)}
data-tippy-content={this.pointsTippy} data-tippy-content={this.pointsTippy}
> >
@ -212,7 +226,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<use xlinkHref="#icon-zap"></use> <use xlinkHref="#icon-zap"></use>
</svg> </svg>
<span class="mr-1">{this.state.score}</span> <span class="mr-1">{this.state.score}</span>
</span> </button>
<span className="mr-1"></span> <span className="mr-1"></span>
<span> <span>
<MomentTime data={node.comment} /> <MomentTime data={node.comment} />
@ -225,6 +239,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
edit edit
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
focus
/> />
)} )}
{!this.state.showEdit && !this.state.collapsed && ( {!this.state.showEdit && !this.state.collapsed && (
@ -693,6 +708,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
node={node} node={node}
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
focus
/> />
)} )}
{node.children && !this.state.collapsed && ( {node.children && !this.state.collapsed && (

@ -160,7 +160,7 @@ export class Communities extends Component<any, CommunitiesState> {
</button> </button>
)} )}
{this.state.communities.length == communityLimit && ( {this.state.communities.length > 0 && (
<button <button
class="btn btn-sm btn-secondary" class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)} onClick={linkEvent(this, this.nextPage)}

@ -260,7 +260,7 @@ export class Community extends Component<any, State> {
{i18n.t('prev')} {i18n.t('prev')}
</button> </button>
)} )}
{this.state.posts.length == fetchLimit && ( {this.state.posts.length > 0 && (
<button <button
class="btn btn-sm btn-secondary" class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)} onClick={linkEvent(this, this.nextPage)}

@ -9,7 +9,7 @@ import {
GetSiteResponse, GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { toast, wsJsonToRes } from '../utils'; import { toast, wsJsonToRes } from '../utils';
import { WebSocketService } from '../services'; import { WebSocketService, UserService } from '../services';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
interface CreateCommunityState { interface CreateCommunityState {
@ -26,6 +26,11 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
this.handleCommunityCreate = this.handleCommunityCreate.bind(this); this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
this.state = this.emptyState; this.state = this.emptyState;
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
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(

@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { toast, wsJsonToRes } from '../utils'; import { toast, wsJsonToRes } from '../utils';
import { WebSocketService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
UserOperation, UserOperation,
PostFormParams, PostFormParams,
@ -41,6 +41,11 @@ export class CreatePost extends Component<any, CreatePostState> {
this.handlePostCreate = this.handlePostCreate.bind(this); this.handlePostCreate = this.handlePostCreate.bind(this);
this.state = this.emptyState; this.state = this.emptyState;
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
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(

@ -2,7 +2,7 @@ import { Component } from 'inferno';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { PrivateMessageForm } from './private-message-form'; import { PrivateMessageForm } from './private-message-form';
import { WebSocketService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
UserOperation, UserOperation,
WebSocketJsonResponse, WebSocketJsonResponse,
@ -20,6 +20,11 @@ export class CreatePrivateMessage extends Component<any, any> {
this this
); );
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
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(

@ -267,6 +267,7 @@ export class Inbox extends Component<any, InboxState> {
nodes={[{ comment: i }]} nodes={[{ comment: i }]}
noIndent noIndent
markable markable
showCommunity
showContext showContext
enableDownvotes={this.state.enableDownvotes} enableDownvotes={this.state.enableDownvotes}
/> />
@ -285,6 +286,7 @@ export class Inbox extends Component<any, InboxState> {
nodes={commentsToFlatNodes(this.state.replies)} nodes={commentsToFlatNodes(this.state.replies)}
noIndent noIndent
markable markable
showCommunity
showContext showContext
enableDownvotes={this.state.enableDownvotes} enableDownvotes={this.state.enableDownvotes}
/> />
@ -300,6 +302,7 @@ export class Inbox extends Component<any, InboxState> {
nodes={[{ comment: mention }]} nodes={[{ comment: mention }]}
noIndent noIndent
markable markable
showCommunity
showContext showContext
enableDownvotes={this.state.enableDownvotes} enableDownvotes={this.state.enableDownvotes}
/> />
@ -329,12 +332,14 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('prev')} {i18n.t('prev')}
</button> </button>
)} )}
<button {this.unreadCount() > 0 && (
class="btn btn-sm btn-secondary" <button
onClick={linkEvent(this, this.nextPage)} class="btn btn-sm btn-secondary"
> onClick={linkEvent(this, this.nextPage)}
{i18n.t('next')} >
</button> {i18n.t('next')}
</button>
)}
</div> </div>
); );
} }
@ -534,15 +539,19 @@ export class Inbox extends Component<any, InboxState> {
} }
sendUnreadCount() { sendUnreadCount() {
let count = UserService.Instance.user.unreadCount = this.unreadCount();
UserService.Instance.sub.next({
user: UserService.Instance.user,
});
}
unreadCount(): number {
return (
this.state.replies.filter(r => !r.read).length + this.state.replies.filter(r => !r.read).length +
this.state.mentions.filter(r => !r.read).length + this.state.mentions.filter(r => !r.read).length +
this.state.messages.filter( this.state.messages.filter(
r => !r.read && r.creator_id !== UserService.Instance.user.id r => !r.read && r.creator_id !== UserService.Instance.user.id
).length; ).length
UserService.Instance.user.unreadCount = count; );
UserService.Instance.sub.next({
user: UserService.Instance.user,
});
} }
} }

@ -373,17 +373,21 @@ export class Main extends Component<any, MainState> {
# #
</a> </a>
<a href="https://en.wikipedia.org/wiki/Fediverse">#</a> <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
<br></br> <br class="big"></br>
<code>#</code> <code>#</code>
<br></br> <br></br>
<b>#</b> <b>#</b>
<br></br> <br class="big"></br>
<a href={repoUrl}>#</a> <a href={repoUrl}>#</a>
<br></br> <br class="big"></br>
<a href="https://www.rust-lang.org">#</a> <a href="https://www.rust-lang.org">#</a>
<a href="https://actix.rs/">#</a> <a href="https://actix.rs/">#</a>
<a href="https://infernojs.org">#</a> <a href="https://infernojs.org">#</a>
<a href="https://www.typescriptlang.org/">#</a> <a href="https://www.typescriptlang.org/">#</a>
<br class="big"></br>
<a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
#
</a>
</T> </T>
</p> </p>
</div> </div>
@ -493,7 +497,7 @@ export class Main extends Component<any, MainState> {
{i18n.t('prev')} {i18n.t('prev')}
</button> </button>
)} )}
{this.state.posts.length == fetchLimit && ( {this.state.posts.length > 0 && (
<button <button
class="btn btn-sm btn-secondary" class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)} onClick={linkEvent(this, this.nextPage)}

@ -33,14 +33,14 @@ import {
randomStr, randomStr,
setupTribute, setupTribute,
setupTippy, setupTippy,
emojiPicker,
hostname, hostname,
pictrsDeleteToast, pictrsDeleteToast,
validTitle,
} from '../utils'; } from '../utils';
import autosize from 'autosize'; import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name'; import emojiShortName from 'emoji-short-name';
import Selectr from 'mobius1-selectr'; import Choices from 'choices.js';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
const MAX_POST_TITLE_LENGTH = 200; const MAX_POST_TITLE_LENGTH = 200;
@ -70,6 +70,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
private id = `post-form-${randomStr()}`; private id = `post-form-${randomStr()}`;
private tribute: Tribute; private tribute: Tribute;
private subscription: Subscription; private subscription: Subscription;
private choices: Choices;
private emptyState: PostFormState = { private emptyState: PostFormState = {
postForm: { postForm: {
name: null, name: null,
@ -95,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this); this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.tribute = setupTribute(); this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState; this.state = this.emptyState;
@ -166,6 +166,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
this.choices && this.choices.destroy();
window.onbeforeunload = null; window.onbeforeunload = null;
} }
@ -271,12 +272,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
value={this.state.postForm.name} value={this.state.postForm.name}
id="post-title" id="post-title"
onInput={linkEvent(this, this.handlePostNameChange)} onInput={linkEvent(this, this.handlePostNameChange)}
class="form-control" class={`form-control ${
!validTitle(this.state.postForm.name) && 'is-invalid'
}`}
required required
rows={2} rows={2}
minLength={3} minLength={3}
maxLength={MAX_POST_TITLE_LENGTH} maxLength={MAX_POST_TITLE_LENGTH}
/> />
{!validTitle(this.state.postForm.name) && (
<div class="invalid-feedback">
{i18n.t('invalid_post_title')}
</div>
)}
{this.state.suggestedPosts.length > 0 && ( {this.state.suggestedPosts.length > 0 && (
<> <>
<div class="my-1 text-muted small font-weight-bold"> <div class="my-1 text-muted small font-weight-bold">
@ -332,15 +340,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<use xlinkHref="#icon-help-circle"></use> <use xlinkHref="#icon-help-circle"></use>
</svg> </svg>
</a> </a>
<span
onClick={linkEvent(this, this.handleEmojiPickerClick)}
class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
data-tippy-content={i18n.t('emoji_picker')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-smile"></use>
</svg>
</span>
</div> </div>
</div> </div>
{!this.props.post && ( {!this.props.post && (
@ -420,20 +419,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
); );
} }
setupEmojiPicker() {
emojiPicker.on('emoji', twemojiHtmlStr => {
if (this.state.postForm.body == null) {
this.state.postForm.body = '';
}
var el = document.createElement('div');
el.innerHTML = twemojiHtmlStr;
let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
let shortName = `:${emojiShortName[nativeUnicode]}:`;
this.state.postForm.body += shortName;
this.setState(this.state);
});
}
handlePostSubmit(i: PostForm, event: any) { handlePostSubmit(i: PostForm, event: any) {
event.preventDefault(); event.preventDefault();
@ -596,10 +581,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
}); });
} }
handleEmojiPickerClick(_i: PostForm, event: any) {
emojiPicker.togglePicker(event.target);
}
parseMessage(msg: WebSocketJsonResponse) { parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg); let res = wsJsonToRes(msg);
if (msg.error) { if (msg.error) {
@ -625,11 +606,45 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
// Set up select searching // Set up select searching
let selectId: any = document.getElementById('post-community'); let selectId: any = document.getElementById('post-community');
if (selectId) { if (selectId) {
let selector = new Selectr(selectId, { nativeDropdown: false }); this.choices = new Choices(selectId, {
selector.on('selectr.select', option => { shouldSort: false,
this.state.postForm.community_id = Number(option.value); classNames: {
this.setState(this.state); containerOuter: 'choices',
containerInner: 'choices__inner bg-secondary border-0',
input: 'form-control',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item bg-secondary',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemChoice: 'choices__item--choice',
placeholder: 'choices__placeholder',
group: 'choices__group',
groupHeading: 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'text-info',
selectedState: 'text-info',
flippedState: 'is-flipped',
loadingState: 'is-loading',
noResults: 'has-no-results',
noChoices: 'has-no-choices',
},
}); });
this.choices.passedElement.element.addEventListener(
'choice',
(e: any) => {
this.state.postForm.community_id = Number(e.detail.choice.value);
this.setState(this.state);
},
false
);
} }
} else if (res.op == UserOperation.CreatePost) { } else if (res.op == UserOperation.CreatePost) {
let data = res.data as PostResponse; let data = res.data as PostResponse;

@ -33,6 +33,7 @@ import {
setupTippy, setupTippy,
hostname, hostname,
previewLines, previewLines,
toast,
} from '../utils'; } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -434,8 +435,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
id: post.creator_id, id: post.creator_id,
local: post.creator_local, local: post.creator_local,
actor_id: post.creator_actor_id, actor_id: post.creator_actor_id,
published: post.creator_published,
}} }}
/> />
{this.isMod && ( {this.isMod && (
<span className="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{i18n.t('mod')} {i18n.t('mod')}
@ -1030,6 +1033,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handlePostLike(i: PostListing) { handlePostLike(i: PostListing) {
if (!UserService.Instance.user) {
this.context.router.history.push(`/login`);
}
let new_vote = i.state.my_vote == 1 ? 0 : 1; let new_vote = i.state.my_vote == 1 ? 0 : 1;
if (i.state.my_vote == 1) { if (i.state.my_vote == 1) {
@ -1057,6 +1064,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handlePostDisLike(i: PostListing) { handlePostDisLike(i: PostListing) {
if (!UserService.Instance.user) {
this.context.router.history.push(`/login`);
}
let new_vote = i.state.my_vote == -1 ? 0 : -1; let new_vote = i.state.my_vote == -1 ? 0 : -1;
if (i.state.my_vote == 1) { if (i.state.my_vote == 1) {

@ -11,6 +11,7 @@ import {
CommentForm as CommentFormI, CommentForm as CommentFormI,
CommentResponse, CommentResponse,
CommentSortType, CommentSortType,
CommentViewType,
CommunityUser, CommunityUser,
CommunityResponse, CommunityResponse,
CommentNode as CommentNodeI, CommentNode as CommentNodeI,
@ -49,6 +50,7 @@ interface PostState {
post: PostI; post: PostI;
comments: Array<Comment>; comments: Array<Comment>;
commentSort: CommentSortType; commentSort: CommentSortType;
commentViewType: CommentViewType;
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
online: number; online: number;
@ -65,6 +67,7 @@ export class Post extends Component<any, PostState> {
post: null, post: null,
comments: [], comments: [],
commentSort: CommentSortType.Hot, commentSort: CommentSortType.Hot,
commentViewType: CommentViewType.Tree,
community: null, community: null,
moderators: [], moderators: [],
online: null, online: null,
@ -208,12 +211,12 @@ export class Post extends Component<any, PostState> {
disabled={this.state.post.locked} disabled={this.state.post.locked}
/> />
{this.state.comments.length > 0 && this.sortRadios()} {this.state.comments.length > 0 && this.sortRadios()}
{this.commentsTree()} {this.state.commentViewType == CommentViewType.Tree &&
</div> this.commentsTree()}
<div class="col-12 col-sm-12 col-md-4"> {this.state.commentViewType == CommentViewType.Chat &&
{this.state.comments.length > 0 && this.newComments()} this.commentsFlat()}
{this.sidebar()}
</div> </div>
<div class="col-12 col-sm-12 col-md-4">{this.sidebar()}</div>
</div> </div>
)} )}
</div> </div>
@ -222,79 +225,94 @@ export class Post extends Component<any, PostState> {
sortRadios() { sortRadios() {
return ( return (
<div class="btn-group btn-group-toggle mb-2"> <>
<label <div class="btn-group btn-group-toggle mr-3 mb-2">
className={`btn btn-sm btn-secondary pointer ${ <label
this.state.commentSort === CommentSortType.Hot && 'active' className={`btn btn-sm btn-secondary pointer ${
}`} this.state.commentSort === CommentSortType.Hot && 'active'
> }`}
{i18n.t('hot')} >
<input {i18n.t('hot')}
type="radio" <input
value={CommentSortType.Hot} type="radio"
checked={this.state.commentSort === CommentSortType.Hot} value={CommentSortType.Hot}
onChange={linkEvent(this, this.handleCommentSortChange)} checked={this.state.commentSort === CommentSortType.Hot}
/> onChange={linkEvent(this, this.handleCommentSortChange)}
</label> />
<label </label>
className={`btn btn-sm btn-secondary pointer ${ <label
this.state.commentSort === CommentSortType.Top && 'active' className={`btn btn-sm btn-secondary pointer ${
}`} this.state.commentSort === CommentSortType.Top && 'active'
> }`}
{i18n.t('top')} >
<input {i18n.t('top')}
type="radio" <input
value={CommentSortType.Top} type="radio"
checked={this.state.commentSort === CommentSortType.Top} value={CommentSortType.Top}
onChange={linkEvent(this, this.handleCommentSortChange)} checked={this.state.commentSort === CommentSortType.Top}
/> onChange={linkEvent(this, this.handleCommentSortChange)}
</label> />
<label </label>
className={`btn btn-sm btn-secondary pointer ${ <label
this.state.commentSort === CommentSortType.New && 'active' className={`btn btn-sm btn-secondary pointer ${
}`} this.state.commentSort === CommentSortType.New && 'active'
> }`}
{i18n.t('new')} >
<input {i18n.t('new')}
type="radio" <input
value={CommentSortType.New} type="radio"
checked={this.state.commentSort === CommentSortType.New} value={CommentSortType.New}
onChange={linkEvent(this, this.handleCommentSortChange)} checked={this.state.commentSort === CommentSortType.New}
/> onChange={linkEvent(this, this.handleCommentSortChange)}
</label> />
<label </label>
className={`btn btn-sm btn-secondary pointer ${ <label
this.state.commentSort === CommentSortType.Old && 'active' className={`btn btn-sm btn-secondary pointer ${
}`} this.state.commentSort === CommentSortType.Old && 'active'
> }`}
{i18n.t('old')} >
<input {i18n.t('old')}
type="radio" <input
value={CommentSortType.Old} type="radio"
checked={this.state.commentSort === CommentSortType.Old} value={CommentSortType.Old}
onChange={linkEvent(this, this.handleCommentSortChange)} checked={this.state.commentSort === CommentSortType.Old}
/> onChange={linkEvent(this, this.handleCommentSortChange)}
</label> />
</div> </label>
</div>
<div class="btn-group btn-group-toggle mb-2">
<label
className={`btn btn-sm btn-secondary pointer ${
this.state.commentViewType === CommentViewType.Chat && 'active'
}`}
>
{i18n.t('chat')}
<input
type="radio"
value={CommentViewType.Chat}
checked={this.state.commentViewType === CommentViewType.Chat}
onChange={linkEvent(this, this.handleCommentViewTypeChange)}
/>
</label>
</div>
</>
); );
} }
newComments() { commentsFlat() {
return ( return (
<div class="d-none d-md-block new-comments mb-3 card border-secondary"> <div>
<div class="card-body small"> <CommentNodes
<h6>{i18n.t('recent_comments')}</h6> nodes={commentsToFlatNodes(this.state.comments)}
<CommentNodes noIndent
nodes={commentsToFlatNodes(this.state.comments)} locked={this.state.post.locked}
noIndent moderators={this.state.moderators}
locked={this.state.post.locked} admins={this.state.siteRes.admins}
moderators={this.state.moderators} postCreatorId={this.state.post.creator_id}
admins={this.state.siteRes.admins} showContext
postCreatorId={this.state.post.creator_id} enableDownvotes={this.state.siteRes.site.enable_downvotes}
showContext sort={this.state.commentSort}
enableDownvotes={this.state.siteRes.site.enable_downvotes} />
/>
</div>
</div> </div>
); );
} }
@ -315,6 +333,13 @@ export class Post extends Component<any, PostState> {
handleCommentSortChange(i: Post, event: any) { handleCommentSortChange(i: Post, event: any) {
i.state.commentSort = Number(event.target.value); i.state.commentSort = Number(event.target.value);
i.state.commentViewType = CommentViewType.Tree;
i.setState(i.state);
}
handleCommentViewTypeChange(i: Post, event: any) {
i.state.commentViewType = Number(event.target.value);
i.state.commentSort = CommentSortType.New;
i.setState(i.state); i.setState(i.state);
} }

@ -148,7 +148,7 @@ export class Search extends Component<any, SearchState> {
{this.state.type_ == SearchType.Posts && this.posts()} {this.state.type_ == SearchType.Posts && this.posts()}
{this.state.type_ == SearchType.Communities && this.communities()} {this.state.type_ == SearchType.Communities && this.communities()}
{this.state.type_ == SearchType.Users && this.users()} {this.state.type_ == SearchType.Users && this.users()}
{this.noResults()} {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
{this.paginator()} {this.paginator()}
</div> </div>
); );
@ -275,6 +275,7 @@ export class Search extends Component<any, SearchState> {
{i.type_ == 'users' && ( {i.type_ == 'users' && (
<div> <div>
<span> <span>
@
<UserListing <UserListing
user={{ user={{
name: (i.data as UserView).name, name: (i.data as UserView).name,
@ -282,9 +283,9 @@ export class Search extends Component<any, SearchState> {
}} }}
/> />
</span> </span>
<span>{` - ${ <span>{` - ${i18n.t('number_of_comments', {
(i.data as UserView).comment_score count: (i.data as UserView).number_of_comments,
} comment karma`}</span> })}`}</span>
</div> </div>
)} )}
</div> </div>
@ -359,12 +360,17 @@ export class Search extends Component<any, SearchState> {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<span> <span>
<Link @
className="text-info" <UserListing
to={`/u/${user.name}`} user={{
>{`/u/${user.name}`}</Link> name: user.name,
avatar: user.avatar,
}}
/>
</span> </span>
<span>{` - ${user.comment_score} comment karma`}</span> <span>{` - ${i18n.t('number_of_comments', {
count: user.number_of_comments,
})}`}</span>
</div> </div>
</div> </div>
))} ))}
@ -383,26 +389,26 @@ export class Search extends Component<any, SearchState> {
{i18n.t('prev')} {i18n.t('prev')}
</button> </button>
)} )}
<button
class="btn btn-sm btn-secondary" {this.resultsCount() > 0 && (
onClick={linkEvent(this, this.nextPage)} <button
> class="btn btn-sm btn-secondary"
{i18n.t('next')} onClick={linkEvent(this, this.nextPage)}
</button> >
{i18n.t('next')}
</button>
)}
</div> </div>
); );
} }
noResults() { resultsCount(): number {
let res = this.state.searchResponse; let res = this.state.searchResponse;
return ( return (
<div> res.posts.length +
{res && res.comments.length +
res.posts.length == 0 && res.communities.length +
res.comments.length == 0 && res.users.length
res.communities.length == 0 &&
res.users.length == 0 && <span>{i18n.t('no_results')}</span>}
</div>
); );
} }

File diff suppressed because one or more lines are too long

@ -1,7 +1,13 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { UserView } from '../interfaces'; import { UserView } from '../interfaces';
import { pictrsAvatarThumbnail, showAvatars, hostname } from '../utils'; import {
pictrsAvatarThumbnail,
showAvatars,
hostname,
isCakeDay,
} from '../utils';
import { CakeDay } from './cake-day';
interface UserOther { interface UserOther {
name: string; name: string;
@ -9,6 +15,7 @@ interface UserOther {
avatar?: string; avatar?: string;
local?: boolean; local?: boolean;
actor_id?: string; actor_id?: string;
published?: string;
} }
interface UserListingProps { interface UserListingProps {
@ -35,17 +42,21 @@ export class UserListing extends Component<UserListingProps, any> {
} }
return ( return (
<Link className="text-body font-weight-bold" to={link}> <>
{user.avatar && showAvatars() && ( <Link className="text-body font-weight-bold" to={link}>
<img {user.avatar && showAvatars() && (
height="32" <img
width="32" height="32"
src={pictrsAvatarThumbnail(user.avatar)} width="32"
class="rounded-circle mr-2" src={pictrsAvatarThumbnail(user.avatar)}
/> class="rounded-circle mr-2"
)} />
<span>{name_}</span> )}
</Link> <span>{name_}</span>
</Link>
{isCakeDay(user.published) && <CakeDay creatorName={name_} />}
</>
); );
} }
} }

@ -48,6 +48,7 @@ import { ListingTypeSelect } from './listing-type-select';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import moment from 'moment';
enum View { enum View {
Overview, Overview,
@ -382,6 +383,7 @@ export class User extends Component<any, UserState> {
nodes={[{ comment: i.data as Comment }]} nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins} admins={this.state.admins}
noIndent noIndent
showCommunity
showContext showContext
enableDownvotes={this.state.site.enable_downvotes} enableDownvotes={this.state.site.enable_downvotes}
/> />
@ -399,6 +401,7 @@ export class User extends Component<any, UserState> {
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
admins={this.state.admins} admins={this.state.admins}
noIndent noIndent
showCommunity
showContext showContext
enableDownvotes={this.state.site.enable_downvotes} enableDownvotes={this.state.site.enable_downvotes}
/> />
@ -440,6 +443,15 @@ export class User extends Component<any, UserState> {
)} )}
</ul> </ul>
</h5> </h5>
<div className="d-flex align-items-center mb-2">
<svg class="icon">
<use xlinkHref="#icon-cake"></use>
</svg>
<span className="ml-2">
{i18n.t('cake_day_title')}{' '}
{moment.utc(user.published).local().format('MMM DD, YYYY')}
</span>
</div>
<div> <div>
{i18n.t('joined')} <MomentTime data={user} showAgo /> {i18n.t('joined')} <MomentTime data={user} showAgo />
</div> </div>
@ -525,7 +537,7 @@ export class User extends Component<any, UserState> {
htmlFor="file-upload" htmlFor="file-upload"
class="pointer ml-4 text-muted small font-weight-bold" class="pointer ml-4 text-muted small font-weight-bold"
> >
{!this.state.userSettingsForm.avatar ? ( {!this.checkSettingsAvatar ? (
<span class="btn btn-sm btn-secondary"> <span class="btn btn-sm btn-secondary">
{i18n.t('upload_avatar')} {i18n.t('upload_avatar')}
</span> </span>
@ -549,6 +561,18 @@ export class User extends Component<any, UserState> {
/> />
</form> </form>
</div> </div>
{this.checkSettingsAvatar && (
<div class="form-group">
<button
class="btn btn-secondary btn-block"
onClick={linkEvent(this, this.removeAvatar)}
>
{`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
'avatar'
)}`}
</button>
</div>
)}
<div class="form-group"> <div class="form-group">
<label>{i18n.t('language')}</label> <label>{i18n.t('language')}</label>
<select <select
@ -883,12 +907,14 @@ export class User extends Component<any, UserState> {
{i18n.t('prev')} {i18n.t('prev')}
</button> </button>
)} )}
<button {this.state.comments.length + this.state.posts.length > 0 && (
class="btn btn-sm btn-secondary" <button
onClick={linkEvent(this, this.nextPage)} class="btn btn-sm btn-secondary"
> onClick={linkEvent(this, this.nextPage)}
{i18n.t('next')} >
</button> {i18n.t('next')}
</button>
)}
</div> </div>
); );
} }
@ -1061,6 +1087,22 @@ export class User extends Component<any, UserState> {
}); });
} }
removeAvatar(i: User, event: any) {
event.preventDefault();
i.state.userSettingsLoading = true;
i.state.userSettingsForm.avatar = '';
i.setState(i.state);
WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
}
get checkSettingsAvatar(): boolean {
return (
this.state.userSettingsForm.avatar &&
this.state.userSettingsForm.avatar != ''
);
}
handleUserSettingsSubmit(i: User, event: any) { handleUserSettingsSubmit(i: User, event: any) {
event.preventDefault(); event.preventDefault();
i.state.userSettingsLoading = true; i.state.userSettingsLoading = true;
@ -1178,7 +1220,6 @@ export class User extends Component<any, UserState> {
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.SaveUserSettings) { } else if (res.op == UserOperation.SaveUserSettings) {
let data = res.data as LoginResponse; let data = res.data as LoginResponse;
this.state = this.emptyState;
this.state.userSettingsLoading = false; this.state.userSettingsLoading = false;
this.setState(this.state); this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);

2
ui/src/index.html vendored

@ -13,7 +13,7 @@
<!-- Styles --> <!-- Styles -->
<link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/selectr.min.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/choices.min.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/tippy.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/tippy.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/litely.min.css" id="default-light" media="(prefers-color-scheme: light)" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/themes/litely.min.css" id="default-light" media="(prefers-color-scheme: light)" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="default-dark" media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="default-dark" media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)" />

@ -54,6 +54,11 @@ export enum CommentSortType {
Old, Old,
} }
export enum CommentViewType {
Tree,
Chat,
}
export enum ListingType { export enum ListingType {
All, All,
Subscribed, Subscribed,
@ -183,6 +188,7 @@ export interface Post {
creator_actor_id: string; creator_actor_id: string;
creator_local: boolean; creator_local: boolean;
creator_name: string; creator_name: string;
creator_published: string;
creator_avatar?: string; creator_avatar?: string;
community_actor_id: string; community_actor_id: string;
community_local: boolean; community_local: boolean;
@ -210,6 +216,7 @@ export interface Comment {
local: boolean; local: boolean;
creator_id: number; creator_id: number;
post_id: number; post_id: number;
post_name: string;
parent_id?: number; parent_id?: number;
content: string; content: string;
removed: boolean; removed: boolean;
@ -227,6 +234,7 @@ export interface Comment {
creator_local: boolean; creator_local: boolean;
creator_name: string; creator_name: string;
creator_avatar?: string; creator_avatar?: string;
creator_published: string;
score: number; score: number;
upvotes: number; upvotes: number;
downvotes: number; downvotes: number;

40
ui/src/utils.ts vendored

@ -51,11 +51,10 @@ import Tribute from 'tributejs/src/Tribute.js';
import markdown_it from 'markdown-it'; import markdown_it from 'markdown-it';
import markdownitEmoji from 'markdown-it-emoji/light'; import markdownitEmoji from 'markdown-it-emoji/light';
import markdown_it_container from 'markdown-it-container'; import markdown_it_container from 'markdown-it-container';
import twemoji from 'twemoji';
import emojiShortName from 'emoji-short-name'; import emojiShortName from 'emoji-short-name';
import Toastify from 'toastify-js'; import Toastify from 'toastify-js';
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import EmojiButton from '@joeattardi/emoji-button'; import moment from 'moment';
export const repoUrl = 'https://github.com/LemmyNet/lemmy'; export const repoUrl = 'https://github.com/LemmyNet/lemmy';
export const helpGuideUrl = '/docs/about_guide.html'; export const helpGuideUrl = '/docs/about_guide.html';
@ -114,14 +113,6 @@ export const themes = [
'litely', 'litely',
]; ];
export const emojiPicker = new EmojiButton({
// Use the emojiShortName from native
style: 'twemoji',
theme: 'dark',
position: 'auto-start',
// TODO i18n
});
const DEFAULT_ALPHABET = const DEFAULT_ALPHABET =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
@ -178,10 +169,6 @@ export const md = new markdown_it({
defs: objectFlip(emojiShortName), defs: objectFlip(emojiShortName),
}); });
md.renderer.rules.emoji = function (token, idx) {
return twemoji.parse(token[idx].content);
};
export function hotRankComment(comment: Comment): number { export function hotRankComment(comment: Comment): number {
return hotRank(comment.score, comment.published); return hotRank(comment.score, comment.published);
} }
@ -501,6 +488,19 @@ export function showAvatars(): boolean {
); );
} }
export function isCakeDay(published: string): boolean {
// moment(undefined) or moment.utc(undefined) returns the current date/time
// moment(null) or moment.utc(null) returns null
const userCreationDate = moment.utc(published || null).local();
const currentDate = moment(new Date());
return (
userCreationDate.date() === currentDate.date() &&
userCreationDate.month() === currentDate.month() &&
userCreationDate.year() !== currentDate.year()
);
}
// Converts to image thumbnail // Converts to image thumbnail
export function pictrsImage(hash: string, thumbnail: boolean = false): string { export function pictrsImage(hash: string, thumbnail: boolean = false): string {
let root = `/pictrs/image`; let root = `/pictrs/image`;
@ -590,8 +590,7 @@ export function setupTribute(): Tribute {
trigger: ':', trigger: ':',
menuItemTemplate: (item: any) => { menuItemTemplate: (item: any) => {
let shortName = `:${item.original.key}:`; let shortName = `:${item.original.key}:`;
let twemojiIcon = twemoji.parse(item.original.val); return `${item.original.val} ${shortName}`;
return `${twemojiIcon} ${shortName}`;
}, },
selectTemplate: (item: any) => { selectTemplate: (item: any) => {
return `:${item.original.key}:`; return `:${item.original.key}:`;
@ -988,3 +987,12 @@ function canUseWebP() {
// // very old browser like IE 8, canvas not supported // // very old browser like IE 8, canvas not supported
// return false; // return false;
} }
export function validTitle(title?: string): boolean {
// Initial title is null, minimum length is taken care of by textarea's minLength={3}
if (title === null || title.length < 3) return true;
const regex = new RegExp(/.*\S.*/, 'g');
return regex.test(title);
}

2
ui/src/version.ts vendored

@ -1 +1 @@
export const version: string = 'v0.7.13'; export const version: string = 'v0.7.19';

@ -217,9 +217,10 @@
"no": "no", "no": "no",
"powered_by": "Powered by", "powered_by": "Powered by",
"landing_0": "landing_0":
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
"not_logged_in": "Not logged in.", "not_logged_in": "Not logged in.",
"logged_in": "Logged in.", "logged_in": "Logged in.",
"must_login": "You must <1>log in or register</1> to comment.",
"site_saved": "Site Saved.", "site_saved": "Site Saved.",
"community_ban": "You have been banned from this community.", "community_ban": "You have been banned from this community.",
"site_ban": "You have been banned from the site", "site_ban": "You have been banned from the site",
@ -265,5 +266,8 @@
"action": "Action", "action": "Action",
"emoji_picker": "Emoji Picker", "emoji_picker": "Emoji Picker",
"block_leaving": "Are you sure you want to leave?", "block_leaving": "Are you sure you want to leave?",
"what_is": "What is" "what_is": "What is",
"cake_day_title": "Cake day:",
"cake_day_info": "It's {{ creator_name }}'s cake day today!",
"invalid_post_title": "Invalid post title"
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save