mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-10-01 01:36:12 -04:00
Merge remote-tracking branch 'LemmyNet/master'
This commit is contained in:
commit
d71897620c
3
.travis.yml
vendored
3
.travis.yml
vendored
@ -24,10 +24,11 @@ script:
|
||||
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
|
||||
- cargo install diesel_cli --no-default-features --features postgres --force
|
||||
- diesel migration run
|
||||
- cargo test
|
||||
- cargo test --workspace
|
||||
env:
|
||||
global:
|
||||
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
- RUST_TEST_THREADS=1
|
||||
|
||||
addons:
|
||||
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
@ -1 +1 @@
|
||||
v0.7.13
|
||||
v0.7.19
|
||||
|
7
docker/dev/Dockerfile
vendored
7
docker/dev/Dockerfile
vendored
@ -18,11 +18,12 @@ RUN sudo chown -R rust:rust .
|
||||
RUN USER=root cargo new server
|
||||
WORKDIR /app/server
|
||||
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 \
|
||||
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
||||
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
||||
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/migrations ./migrations/
|
||||
|
||||
|
11
docker/prod/Dockerfile
vendored
11
docker/prod/Dockerfile
vendored
@ -10,14 +10,15 @@ WORKDIR /app
|
||||
RUN sudo chown -R rust:rust .
|
||||
RUN USER=root cargo new server
|
||||
WORKDIR /app/server
|
||||
COPY --chown=rust:rust server/Cargo.toml server/Cargo.lock ./
|
||||
#RUN sudo chown -R rust:rust .
|
||||
COPY server/Cargo.toml server/Cargo.lock ./
|
||||
COPY server/lemmy_db ./lemmy_db
|
||||
COPY server/lemmy_utils ./lemmy_utils
|
||||
RUN mkdir -p ./src/bin \
|
||||
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
||||
RUN cargo build --release
|
||||
RUN rm -f ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/deps/lemmy_server*
|
||||
COPY --chown=rust:rust server/src ./src/
|
||||
COPY --chown=rust:rust server/migrations ./migrations/
|
||||
RUN find target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR -type f -name "$(echo "lemmy_server" | tr '-' '_')*" -exec touch -t 200001010000 {} +
|
||||
COPY server/src ./src/
|
||||
COPY server/migrations ./migrations/
|
||||
|
||||
# build for release
|
||||
# workaround for https://github.com/rust-lang/rust/issues/62896
|
||||
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
@ -12,7 +12,7 @@ services:
|
||||
restart: always
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.7.13
|
||||
image: dessalines/lemmy:v0.7.19
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
|
2
docs/src/administration_configuration.md
vendored
2
docs/src/administration_configuration.md
vendored
@ -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
|
||||
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
|
||||
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
|
||||
`database.password` with `LEMMY_DATABASE__POOL_SIZE=10`.
|
||||
|
4
docs/src/contributing_tests.md
vendored
4
docs/src/contributing_tests.md
vendored
@ -7,9 +7,7 @@ following commands in the `server` subfolder:
|
||||
|
||||
```bash
|
||||
psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
||||
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
diesel migration run
|
||||
RUST_TEST_THREADS=1 cargo test
|
||||
./test.sh
|
||||
```
|
||||
|
||||
### Federation
|
||||
|
1
docs/src/contributing_websocket_http_api.md
vendored
1
docs/src/contributing_websocket_http_api.md
vendored
@ -1149,6 +1149,7 @@ Post listing types are `All, Subscribed, Community`
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
community_id: Option<i32>,
|
||||
community_name: Option<String>,
|
||||
auth: Option<String>
|
||||
}
|
||||
}
|
||||
|
2
install.sh
vendored
2
install.sh
vendored
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Set the database variable to the default first.
|
||||
|
53
server/Cargo.lock
generated
vendored
53
server/Cargo.lock
generated
vendored
@ -1399,12 +1399,6 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmlescape"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.1"
|
||||
@ -1428,9 +1422,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "09afff6987c7edbed101d1cddd2185786fb0af0dd9c06b654aca73a0a763680f"
|
||||
checksum = "131fc982391a6b37847888b568cbe0e9cd302f1b0015f4f6f4a50234bebd049c"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
@ -1572,6 +1566,21 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "lemmy_server"
|
||||
version = "0.0.1"
|
||||
@ -1589,27 +1598,23 @@ dependencies = [
|
||||
"base64 0.12.3",
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"comrak",
|
||||
"config",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"dotenv",
|
||||
"env_logger",
|
||||
"failure",
|
||||
"futures",
|
||||
"htmlescape",
|
||||
"http",
|
||||
"http-signature-normalization-actix",
|
||||
"itertools",
|
||||
"jsonwebtoken",
|
||||
"lazy_static",
|
||||
"lettre",
|
||||
"lettre_email",
|
||||
"lemmy_db",
|
||||
"lemmy_utils",
|
||||
"log",
|
||||
"openssl",
|
||||
"percent-encoding",
|
||||
"rand 0.7.3",
|
||||
"regex",
|
||||
"rss",
|
||||
"serde 1.0.114",
|
||||
"serde_json",
|
||||
@ -1621,6 +1626,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "lettre"
|
||||
version = "0.9.3"
|
||||
|
19
server/Cargo.toml
vendored
19
server/Cargo.toml
vendored
@ -1,14 +1,21 @@
|
||||
[package]
|
||||
name = "lemmy_server"
|
||||
version = "0.0.1"
|
||||
authors = ["Dessalines <tyhou13@gmx.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"lemmy_utils",
|
||||
"lemmy_db"
|
||||
]
|
||||
|
||||
[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"
|
||||
dotenv = "0.15.0"
|
||||
activitystreams = "0.6.2"
|
||||
@ -31,19 +38,13 @@ rand = "0.7.3"
|
||||
strum = "0.18.0"
|
||||
strum_macros = "0.18.0"
|
||||
jsonwebtoken = "7.0.1"
|
||||
regex = "1.3.5"
|
||||
lazy_static = "1.3.0"
|
||||
lettre = "0.9.3"
|
||||
lettre_email = "0.9.4"
|
||||
rss = "1.9.0"
|
||||
htmlescape = "0.3.1"
|
||||
url = { version = "2.1.1", features = ["serde"] }
|
||||
config = {version = "0.10.1", default-features = false, features = ["hjson"] }
|
||||
percent-encoding = "2.1.0"
|
||||
comrak = "0.7"
|
||||
openssl = "0.10"
|
||||
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"
|
||||
tokio = "0.2.21"
|
||||
futures = "0.3.5"
|
||||
|
3
server/db-init.sh
vendored
3
server/db-init.sh
vendored
@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Default configurations
|
||||
username=lemmy
|
||||
|
2
server/diesel.toml
vendored
2
server/diesel.toml
vendored
@ -2,4 +2,4 @@
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
file = "lemmy_db/src/schema.rs"
|
||||
|
15
server/lemmy_db/Cargo.toml
vendored
Normal file
15
server/lemmy_db/Cargo.toml
vendored
Normal file
@ -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 log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::fmt::Debug;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{Error as IoError, ErrorKind},
|
||||
};
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[table_name = "activity"]
|
||||
@ -55,46 +58,43 @@ impl Crud<ActivityForm> for Activity {
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
fn do_insert_activity<T>(
|
||||
pub fn do_insert_activity<T>(
|
||||
conn: &PgConnection,
|
||||
user_id: i32,
|
||||
data: &T,
|
||||
local: bool,
|
||||
) -> Result<(), LemmyError>
|
||||
) -> Result<Activity, IoError>
|
||||
where
|
||||
T: Serialize + Debug,
|
||||
{
|
||||
debug!("inserting activity for user {}, data {:?}", user_id, &data);
|
||||
let activity_form = ActivityForm {
|
||||
user_id,
|
||||
data: serde_json::to_value(&data)?,
|
||||
local,
|
||||
updated: None,
|
||||
};
|
||||
debug!("inserting activity for user {}, data {:?}", user_id, data);
|
||||
Activity::create(&conn, &activity_form)?;
|
||||
Ok(())
|
||||
let result = Activity::create(&conn, &activity_form);
|
||||
match result {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
format!("Failed to insert activity into database: {}", e),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{super::user::*, *};
|
||||
use crate::db::{establish_unpooled_connection, Crud, ListingType, SortType};
|
||||
use crate::{
|
||||
activity::{Activity, ActivityForm},
|
||||
tests::establish_unpooled_connection,
|
||||
user::{UserForm, User_},
|
||||
Crud,
|
||||
ListingType,
|
||||
SortType,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
db::Crud,
|
||||
schema::{category, category::dsl::*},
|
||||
Crud,
|
||||
};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -52,8 +52,7 @@ impl Category {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::establish_unpooled_connection;
|
||||
use crate::{category::Category, tests::establish_unpooled_connection};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
@ -1,9 +1,5 @@
|
||||
use super::{post::Post, *};
|
||||
use crate::{
|
||||
apub::{make_apub_endpoint, EndpointType},
|
||||
naive_now,
|
||||
schema::{comment, comment_like, comment_saved},
|
||||
};
|
||||
use crate::schema::{comment, comment_like, comment_saved};
|
||||
|
||||
// WITH RECURSIVE MyTree AS (
|
||||
// SELECT * FROM comment WHERE parent_id IS NULL
|
||||
@ -77,12 +73,15 @@ impl Crud<CommentForm> for 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::*;
|
||||
|
||||
let apid = make_apub_endpoint(EndpointType::Comment, &comment_id.to_string()).to_string();
|
||||
diesel::update(comment.find(comment_id))
|
||||
.set(ap_id.eq(apid))
|
||||
.set(ap_id.eq(apub_id))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
@ -204,10 +203,8 @@ impl Saveable<CommentSavedForm> for CommentSaved {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::{community::*, post::*, user::*},
|
||||
*,
|
||||
};
|
||||
use crate::{comment::*, community::*, post::*, tests::establish_unpooled_connection, user::*};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_unpooled_connection();
|
@ -1,5 +1,5 @@
|
||||
// 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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -9,6 +9,7 @@ table! {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
post_id -> Int4,
|
||||
post_name -> Varchar,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
@ -27,6 +28,7 @@ table! {
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
@ -44,6 +46,7 @@ table! {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
post_id -> Int4,
|
||||
post_name -> Varchar,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
@ -62,6 +65,7 @@ table! {
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
@ -82,6 +86,7 @@ pub struct CommentView {
|
||||
pub id: i32,
|
||||
pub creator_id: i32,
|
||||
pub post_id: i32,
|
||||
pub post_name: String,
|
||||
pub parent_id: Option<i32>,
|
||||
pub content: String,
|
||||
pub removed: bool,
|
||||
@ -100,6 +105,7 @@ pub struct CommentView {
|
||||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_published: chrono::NaiveDateTime,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
@ -295,6 +301,7 @@ table! {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
post_id -> Int4,
|
||||
post_name -> Varchar,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
@ -314,6 +321,7 @@ table! {
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
creator_published -> Timestamp,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
@ -334,6 +342,7 @@ pub struct ReplyView {
|
||||
pub id: i32,
|
||||
pub creator_id: i32,
|
||||
pub post_id: i32,
|
||||
pub post_name: String,
|
||||
pub parent_id: Option<i32>,
|
||||
pub content: String,
|
||||
pub removed: bool,
|
||||
@ -353,6 +362,7 @@ pub struct ReplyView {
|
||||
pub creator_local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub creator_published: chrono::NaiveDateTime,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
@ -455,11 +465,17 @@ impl<'a> ReplyQueryBuilder<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::{comment::*, community::*, post::*, user::*},
|
||||
use crate::{
|
||||
comment::*,
|
||||
comment_view::*,
|
||||
community::*,
|
||||
post::*,
|
||||
tests::establish_unpooled_connection,
|
||||
user::*,
|
||||
Crud,
|
||||
Likeable,
|
||||
*,
|
||||
};
|
||||
use crate::db::{establish_unpooled_connection, Crud, Likeable};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
@ -565,6 +581,7 @@ mod tests {
|
||||
content: "A test comment 32".into(),
|
||||
creator_id: inserted_user.id,
|
||||
post_id: inserted_post.id,
|
||||
post_name: inserted_post.name.to_owned(),
|
||||
community_id: inserted_community.id,
|
||||
community_name: inserted_community.name.to_owned(),
|
||||
parent_id: None,
|
||||
@ -576,6 +593,7 @@ mod tests {
|
||||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
||||
@ -598,6 +616,7 @@ mod tests {
|
||||
content: "A test comment 32".into(),
|
||||
creator_id: inserted_user.id,
|
||||
post_id: inserted_post.id,
|
||||
post_name: inserted_post.name.to_owned(),
|
||||
community_id: inserted_community.id,
|
||||
community_name: inserted_community.name.to_owned(),
|
||||
parent_id: None,
|
||||
@ -609,6 +628,7 @@ mod tests {
|
||||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
@ -1,6 +1,9 @@
|
||||
use crate::{
|
||||
db::{Bannable, Crud, Followable, Joinable},
|
||||
schema::{community, community_follower, community_moderator, community_user_ban},
|
||||
Bannable,
|
||||
Crud,
|
||||
Followable,
|
||||
Joinable,
|
||||
};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -232,8 +235,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{super::user::*, *};
|
||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
||||
use crate::{community::*, tests::establish_unpooled_connection, user::*, ListingType, SortType};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
@ -1,5 +1,5 @@
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -299,6 +299,7 @@ impl CommunityModeratorView {
|
||||
use super::community_view::community_moderator_view::dsl::*;
|
||||
community_moderator_view
|
||||
.filter(community_id.eq(from_community_id))
|
||||
.order_by(published)
|
||||
.load::<Self>(conn)
|
||||
}
|
||||
|
||||
@ -306,6 +307,7 @@ impl CommunityModeratorView {
|
||||
use super::community_view::community_moderator_view::dsl::*;
|
||||
community_moderator_view
|
||||
.filter(user_id.eq(from_user_id))
|
||||
.order_by(published)
|
||||
.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 serde::{Deserialize, Serialize};
|
||||
use std::{env, env::VarError};
|
||||
|
||||
pub mod activity;
|
||||
pub mod category;
|
||||
pub mod code_migrations;
|
||||
pub mod comment;
|
||||
pub mod comment_view;
|
||||
pub mod community;
|
||||
@ -16,6 +28,7 @@ pub mod post;
|
||||
pub mod post_view;
|
||||
pub mod private_message;
|
||||
pub mod private_message_view;
|
||||
pub mod schema;
|
||||
pub mod site;
|
||||
pub mod site_view;
|
||||
pub mod user;
|
||||
@ -111,9 +124,8 @@ impl<T> MaybeOptional<T> for Option<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn establish_unpooled_connection() -> PgConnection {
|
||||
let db_url = Settings::get().get_database_url();
|
||||
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
|
||||
pub fn get_database_url_from_env() -> Result<String, VarError> {
|
||||
env::var("LEMMY_DATABASE_URL")
|
||||
}
|
||||
|
||||
#[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);
|
||||
(limit, offset)
|
||||
}
|
||||
|
||||
pub fn naive_now() -> NaiveDateTime {
|
||||
chrono::prelude::Utc::now().naive_utc()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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]
|
||||
fn test_fuzzy_search() {
|
||||
let test = "This is a fuzzy search";
|
@ -1,5 +1,4 @@
|
||||
use crate::{
|
||||
db::Crud,
|
||||
schema::{
|
||||
mod_add,
|
||||
mod_add_community,
|
||||
@ -11,6 +10,7 @@ use crate::{
|
||||
mod_remove_post,
|
||||
mod_sticky_post,
|
||||
},
|
||||
Crud,
|
||||
};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -437,11 +437,16 @@ impl Crud<ModAddForm> for ModAdd {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::{comment::*, community::*, post::*, user::*},
|
||||
*,
|
||||
use crate::{
|
||||
comment::*,
|
||||
community::*,
|
||||
moderator::*,
|
||||
post::*,
|
||||
tests::establish_unpooled_connection,
|
||||
user::*,
|
||||
ListingType,
|
||||
SortType,
|
||||
};
|
||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
||||
|
||||
// use Crud;
|
||||
#[test]
|
@ -1,4 +1,4 @@
|
||||
use crate::db::limit_and_offset;
|
||||
use crate::limit_and_offset;
|
||||
use diesel::{result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
db::Crud,
|
||||
schema::{password_reset_request, password_reset_request::dsl::*},
|
||||
Crud,
|
||||
};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use sha2::{Digest, Sha256};
|
||||
@ -82,7 +82,7 @@ impl PasswordResetRequest {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{super::user::*, *};
|
||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
||||
use crate::{tests::establish_unpooled_connection, ListingType, SortType};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
@ -1,8 +1,10 @@
|
||||
use crate::{
|
||||
apub::{make_apub_endpoint, EndpointType},
|
||||
db::{Crud, Likeable, Readable, Saveable},
|
||||
naive_now,
|
||||
schema::{post, post_like, post_read, post_saved},
|
||||
Crud,
|
||||
Likeable,
|
||||
Readable,
|
||||
Saveable,
|
||||
};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -75,12 +77,11 @@ impl Post {
|
||||
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::*;
|
||||
|
||||
let apid = make_apub_endpoint(EndpointType::Post, &post_id.to_string()).to_string();
|
||||
diesel::update(post.find(post_id))
|
||||
.set(ap_id.eq(apid))
|
||||
.set(ap_id.eq(apub_id))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
@ -241,11 +242,14 @@ impl Readable<PostReadForm> for PostRead {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::{community::*, user::*},
|
||||
*,
|
||||
use crate::{
|
||||
community::*,
|
||||
post::*,
|
||||
tests::establish_unpooled_connection,
|
||||
user::*,
|
||||
ListingType,
|
||||
SortType,
|
||||
};
|
||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
@ -1,5 +1,5 @@
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -28,6 +28,7 @@ table! {
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
@ -75,6 +76,7 @@ table! {
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
@ -125,6 +127,7 @@ pub struct PostView {
|
||||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_published: chrono::NaiveDateTime,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
@ -155,6 +158,7 @@ pub struct PostQueryBuilder<'a> {
|
||||
my_user_id: Option<i32>,
|
||||
for_creator_id: Option<i32>,
|
||||
for_community_id: Option<i32>,
|
||||
for_community_name: Option<String>,
|
||||
search_term: Option<String>,
|
||||
url_search: Option<String>,
|
||||
show_nsfw: bool,
|
||||
@ -178,6 +182,7 @@ impl<'a> PostQueryBuilder<'a> {
|
||||
my_user_id: None,
|
||||
for_creator_id: None,
|
||||
for_community_id: None,
|
||||
for_community_name: None,
|
||||
search_term: None,
|
||||
url_search: None,
|
||||
show_nsfw: true,
|
||||
@ -203,6 +208,11 @@ impl<'a> PostQueryBuilder<'a> {
|
||||
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 {
|
||||
self.for_creator_id = for_creator_id.get_optional();
|
||||
self
|
||||
@ -262,6 +272,11 @@ impl<'a> PostQueryBuilder<'a> {
|
||||
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 {
|
||||
query = query.filter(url.eq(url_search));
|
||||
}
|
||||
@ -364,11 +379,16 @@ impl PostView {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::{community::*, post::*, user::*},
|
||||
use crate::{
|
||||
community::*,
|
||||
post::*,
|
||||
post_view::*,
|
||||
tests::establish_unpooled_connection,
|
||||
user::*,
|
||||
Crud,
|
||||
Likeable,
|
||||
*,
|
||||
};
|
||||
use crate::db::{establish_unpooled_connection, Crud, Likeable};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
@ -499,6 +519,7 @@ mod tests {
|
||||
body: None,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name.to_owned(),
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
@ -548,6 +569,7 @@ mod tests {
|
||||
stickied: false,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name,
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
@ -1,8 +1,4 @@
|
||||
use crate::{
|
||||
apub::{make_apub_endpoint, EndpointType},
|
||||
db::Crud,
|
||||
schema::private_message,
|
||||
};
|
||||
use crate::{schema::private_message, Crud};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -66,16 +62,15 @@ impl Crud<PrivateMessageForm> for 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::*;
|
||||
|
||||
let apid = make_apub_endpoint(
|
||||
EndpointType::PrivateMessage,
|
||||
&private_message_id.to_string(),
|
||||
)
|
||||
.to_string();
|
||||
diesel::update(private_message.find(private_message_id))
|
||||
.set(ap_id.eq(apid))
|
||||
.set(ap_id.eq(apub_id))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
@ -89,8 +84,13 @@ impl PrivateMessage {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{super::user::*, *};
|
||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
||||
use crate::{
|
||||
private_message::*,
|
||||
tests::establish_unpooled_connection,
|
||||
user::*,
|
||||
ListingType,
|
||||
SortType,
|
||||
};
|
||||
|
||||
#[test]
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
@ -47,6 +47,7 @@ table! {
|
||||
deleted -> Nullable<Bool>,
|
||||
ap_id -> Nullable<Varchar>,
|
||||
local -> Nullable<Bool>,
|
||||
post_name -> Nullable<Varchar>,
|
||||
community_id -> Nullable<Int4>,
|
||||
community_actor_id -> Nullable<Varchar>,
|
||||
community_local -> Nullable<Bool>,
|
||||
@ -56,6 +57,7 @@ table! {
|
||||
creator_actor_id -> Nullable<Varchar>,
|
||||
creator_local -> Nullable<Bool>,
|
||||
creator_name -> Nullable<Varchar>,
|
||||
creator_published -> Nullable<Timestamp>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> Nullable<Int8>,
|
||||
upvotes -> Nullable<Int8>,
|
||||
@ -317,6 +319,7 @@ table! {
|
||||
creator_actor_id -> Nullable<Varchar>,
|
||||
creator_local -> Nullable<Bool>,
|
||||
creator_name -> Nullable<Varchar>,
|
||||
creator_published -> Nullable<Timestamp>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
banned -> 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 serde::{Deserialize, Serialize};
|
||||
|
@ -1,14 +1,10 @@
|
||||
use crate::{
|
||||
db::Crud,
|
||||
is_email_regex,
|
||||
naive_now,
|
||||
schema::{user_, user_::dsl::*},
|
||||
settings::Settings,
|
||||
Crud,
|
||||
};
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
|
||||
#[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_ {
|
||||
pub fn jwt(&self) -> Jwt {
|
||||
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> {
|
||||
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn find_by_email_or_username(
|
||||
conn: &PgConnection,
|
||||
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)
|
||||
pub fn get_profile_url(&self, hostname: &str) -> String {
|
||||
format!("https://{}/u/{}", hostname, self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{User_, *};
|
||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
||||
use crate::{tests::establish_unpooled_connection, user::*, ListingType, SortType};
|
||||
|
||||
#[test]
|
||||
fn test_crud() {
|
@ -1,5 +1,5 @@
|
||||
use super::comment::Comment;
|
||||
use crate::{db::Crud, schema::user_mention};
|
||||
use crate::{schema::user_mention, Crud};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -54,11 +54,16 @@ impl Crud<UserMentionForm> for UserMention {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::{comment::*, community::*, post::*, user::*},
|
||||
*,
|
||||
use crate::{
|
||||
comment::*,
|
||||
community::*,
|
||||
post::*,
|
||||
tests::establish_unpooled_connection,
|
||||
user::*,
|
||||
user_mention::*,
|
||||
ListingType,
|
||||
SortType,
|
||||
};
|
||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
||||
|
||||
#[test]
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -11,6 +11,7 @@ table! {
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
post_id -> Int4,
|
||||
post_name -> Varchar,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
@ -47,6 +48,7 @@ table! {
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
post_id -> Int4,
|
||||
post_name -> Varchar,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
@ -86,6 +88,7 @@ pub struct UserMentionView {
|
||||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub post_id: i32,
|
||||
pub post_name: String,
|
||||
pub parent_id: Option<i32>,
|
||||
pub content: String,
|
||||
pub removed: bool,
|
@ -1,5 +1,5 @@
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -157,7 +157,10 @@ impl UserView {
|
||||
|
||||
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||
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> {
|
22
server/lemmy_utils/Cargo.toml
vendored
Normal file
22
server/lemmy_utils/Cargo.toml
vendored
Normal file
@ -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"] }
|
337
server/lemmy_utils/src/lib.rs
Normal file
337
server/lemmy_utils/src/lib.rs
Normal file
@ -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 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: &str = "config/config.hjson";
|
||||
@ -76,12 +76,15 @@ impl Settings {
|
||||
/// 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
|
||||
/// 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> {
|
||||
let mut s = Config::new();
|
||||
|
||||
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)
|
||||
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
|
||||
@ -98,32 +101,31 @@ impl Settings {
|
||||
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 {
|
||||
match env::var("LEMMY_DATABASE_URL") {
|
||||
Ok(url) => url,
|
||||
Err(_) => format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
self.database.user,
|
||||
self.database.password,
|
||||
self.database.host,
|
||||
self.database.port,
|
||||
self.database.database
|
||||
),
|
||||
}
|
||||
format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
self.database.user,
|
||||
self.database.password,
|
||||
self.database.host,
|
||||
self.database.port,
|
||||
self.database.database
|
||||
)
|
||||
}
|
||||
|
||||
pub fn api_endpoint(&self) -> String {
|
||||
format!("{}/api/v1", self.hostname)
|
||||
}
|
||||
|
||||
pub fn read_config_file() -> Result<String, LemmyError> {
|
||||
Ok(fs::read_to_string(CONFIG_FILE)?)
|
||||
pub fn get_config_location() -> String {
|
||||
env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE.to_string())
|
||||
}
|
||||
|
||||
pub fn save_config_file(data: &str) -> Result<String, LemmyError> {
|
||||
fs::write(CONFIG_FILE, data)?;
|
||||
pub fn read_config_file() -> Result<String, Error> {
|
||||
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
|
||||
// From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804
|
388
server/migrations/2020-07-08-202609_add_creator_published/down.sql
vendored
Normal file
388
server/migrations/2020-07-08-202609_add_creator_published/down.sql
vendored
Normal file
@ -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;
|
390
server/migrations/2020-07-08-202609_add_creator_published/up.sql
vendored
Normal file
390
server/migrations/2020-07-08-202609_add_creator_published/up.sql
vendored
Normal file
@ -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;
|
249
server/migrations/2020-07-12-100442_add_post_title_to_comments_view/down.sql
vendored
Normal file
249
server/migrations/2020-07-12-100442_add_post_title_to_comments_view/down.sql
vendored
Normal file
@ -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
|
||||
;
|
254
server/migrations/2020-07-12-100442_add_post_title_to_comments_view/up.sql
vendored
Normal file
254
server/migrations/2020-07-12-100442_add_post_title_to_comments_view/up.sql
vendored
Normal file
@ -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
|
||||
;
|
73
server/src/api/claims.rs
Normal file
73
server/src/api/claims.rs
Normal file
@ -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::{
|
||||
api::{APIError, Oper, Perform},
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
apub::{ApubLikeableType, ApubObjectType},
|
||||
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::{
|
||||
server::{JoinCommunityRoom, SendComment},
|
||||
UserOperation,
|
||||
@ -30,6 +9,31 @@ use crate::{
|
||||
},
|
||||
DbPool,
|
||||
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,
|
||||
};
|
||||
use log::error;
|
||||
@ -155,7 +159,9 @@ impl Perform for Oper<CreateComment> {
|
||||
|
||||
let inserted_comment_id = inserted_comment.id;
|
||||
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?
|
||||
{
|
||||
|
@ -1,26 +1,24 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
api::{APIError, Oper, Perform},
|
||||
apub::{
|
||||
extensions::signatures::generate_actor_keypair,
|
||||
make_apub_endpoint,
|
||||
ActorType,
|
||||
EndpointType,
|
||||
},
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
apub::ActorType,
|
||||
blocking,
|
||||
db::{Bannable, Crud, Followable, Joinable, SortType},
|
||||
is_valid_community_name,
|
||||
naive_from_unix,
|
||||
naive_now,
|
||||
slur_check,
|
||||
slurs_vec_to_str,
|
||||
websocket::{
|
||||
server::{JoinCommunityRoom, SendCommunityRoomMessage},
|
||||
UserOperation,
|
||||
WebsocketInfo,
|
||||
},
|
||||
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 std::str::FromStr;
|
||||
|
@ -1,11 +1,8 @@
|
||||
use crate::{
|
||||
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
|
||||
websocket::WebsocketInfo,
|
||||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
|
||||
use actix_web::client::Client;
|
||||
use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
|
||||
|
||||
pub mod claims;
|
||||
pub mod comment;
|
||||
pub mod community;
|
||||
pub mod post;
|
||||
|
@ -1,27 +1,8 @@
|
||||
use crate::{
|
||||
api::{APIError, Oper, Perform},
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
apub::{ApubLikeableType, ApubObjectType},
|
||||
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,
|
||||
naive_now,
|
||||
slur_check,
|
||||
slurs_vec_to_str,
|
||||
websocket::{
|
||||
server::{JoinCommunityRoom, JoinPostRoom, SendPost},
|
||||
UserOperation,
|
||||
@ -30,6 +11,30 @@ use crate::{
|
||||
DbPool,
|
||||
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 std::str::FromStr;
|
||||
|
||||
@ -71,6 +76,7 @@ pub struct GetPosts {
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
pub community_id: Option<i32>,
|
||||
pub community_name: 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;
|
||||
|
||||
// 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;
|
||||
|
||||
let post_form = PostForm {
|
||||
name: data.name.to_owned(),
|
||||
name: data.name.trim().to_owned(),
|
||||
url: data.url.to_owned(),
|
||||
body: data.body.to_owned(),
|
||||
community_id: data.community_id,
|
||||
@ -191,11 +201,16 @@ impl Perform for Oper<CreatePost> {
|
||||
};
|
||||
|
||||
let inserted_post_id = inserted_post.id;
|
||||
let updated_post =
|
||||
match blocking(pool, move |conn| Post::update_ap_id(conn, inserted_post_id)).await? {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
||||
};
|
||||
let updated_post = match blocking(pool, move |conn| {
|
||||
let apub_id =
|
||||
make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
|
||||
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?;
|
||||
|
||||
@ -361,12 +376,14 @@ impl Perform for Oper<GetPosts> {
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let community_id = data.community_id;
|
||||
let community_name = data.community_name.to_owned();
|
||||
let posts = match blocking(pool, move |conn| {
|
||||
PostQueryBuilder::create(conn)
|
||||
.listing_type(type_)
|
||||
.sort(&sort)
|
||||
.show_nsfw(show_nsfw)
|
||||
.for_community_id(community_id)
|
||||
.for_community_name(community_name)
|
||||
.my_user_id(user_id)
|
||||
.page(page)
|
||||
.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) {
|
||||
Ok(claims) => claims.claims,
|
||||
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 post_form = PostForm {
|
||||
name: data.name.to_owned(),
|
||||
name: data.name.trim().to_owned(),
|
||||
url: data.url.to_owned(),
|
||||
body: data.body.to_owned(),
|
||||
creator_id: data.creator_id.to_owned(),
|
||||
|
@ -1,31 +1,28 @@
|
||||
use super::user::Register;
|
||||
use crate::{
|
||||
api::{APIError, Oper, Perform},
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
apub::fetcher::search_by_apub_id,
|
||||
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},
|
||||
DbPool,
|
||||
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 serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
@ -1,44 +1,7 @@
|
||||
use crate::{
|
||||
api::{APIError, Oper, Perform},
|
||||
apub::{
|
||||
extensions::signatures::generate_actor_keypair,
|
||||
make_apub_endpoint,
|
||||
ApubObjectType,
|
||||
EndpointType,
|
||||
},
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
apub::ApubObjectType,
|
||||
blocking,
|
||||
db::{
|
||||
comment::*,
|
||||
comment_view::*,
|
||||
community::*,
|
||||
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,
|
||||
},
|
||||
generate_random_string,
|
||||
is_valid_username,
|
||||
naive_from_unix,
|
||||
naive_now,
|
||||
remove_slurs,
|
||||
send_email,
|
||||
settings::Settings,
|
||||
slur_check,
|
||||
slurs_vec_to_str,
|
||||
websocket::{
|
||||
server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
|
||||
UserOperation,
|
||||
@ -48,6 +11,43 @@ use crate::{
|
||||
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,
|
||||
is_valid_username,
|
||||
make_apub_endpoint,
|
||||
naive_from_unix,
|
||||
remove_slurs,
|
||||
send_email,
|
||||
settings::Settings,
|
||||
slur_check,
|
||||
slurs_vec_to_str,
|
||||
EndpointType,
|
||||
};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
@ -264,7 +264,7 @@ impl Perform for Oper<Login> {
|
||||
// Fetch that username / email
|
||||
let username_or_email = data.username_or_email.clone();
|
||||
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?
|
||||
{
|
||||
@ -279,7 +279,9 @@ impl Perform for Oper<Login> {
|
||||
}
|
||||
|
||||
// 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
|
||||
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,
|
||||
};
|
||||
|
||||
let avatar = match &data.avatar {
|
||||
Some(avatar) => Some(avatar.to_owned()),
|
||||
None => read_user.avatar,
|
||||
};
|
||||
|
||||
let password_encrypted = match &data.new_password {
|
||||
Some(new_password) => {
|
||||
match &data.new_password_verify {
|
||||
@ -488,7 +495,7 @@ impl Perform for Oper<SaveUserSettings> {
|
||||
name: read_user.name,
|
||||
email,
|
||||
matrix_user_id: data.matrix_user_id.to_owned(),
|
||||
avatar: data.avatar.to_owned(),
|
||||
avatar,
|
||||
password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
@ -527,7 +534,7 @@ impl Perform for Oper<SaveUserSettings> {
|
||||
|
||||
// Return the jwt
|
||||
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
|
||||
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 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?
|
||||
{
|
||||
|
@ -1,12 +1,18 @@
|
||||
use crate::{
|
||||
apub::{extensions::signatures::sign, is_apub_id_valid, ActorType},
|
||||
db::{activity::insert_activity, community::Community, user::User_},
|
||||
apub::{
|
||||
community::do_announce,
|
||||
extensions::signatures::sign,
|
||||
insert_activity,
|
||||
is_apub_id_valid,
|
||||
ActorType,
|
||||
},
|
||||
request::retry_custom,
|
||||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
|
||||
use actix_web::client::Client;
|
||||
use lemmy_db::{community::Community, user::User_};
|
||||
use log::debug;
|
||||
use serde::Serialize;
|
||||
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 community.local {
|
||||
Community::do_announce(activity, &community, creator, client, pool).await?;
|
||||
do_announce(activity, &community, creator, client, pool).await?;
|
||||
} else {
|
||||
send_activity(client, &activity, creator, to).await?;
|
||||
}
|
||||
|
@ -1,35 +1,16 @@
|
||||
use crate::{
|
||||
apub::{
|
||||
activities::{populate_object_props, send_activity_to_community},
|
||||
create_apub_response,
|
||||
create_apub_tombstone_response,
|
||||
create_tombstone,
|
||||
fetch_webfinger_url,
|
||||
create_apub_response, create_apub_tombstone_response, create_tombstone, fetch_webfinger_url,
|
||||
fetcher::{
|
||||
get_or_fetch_and_insert_remote_comment,
|
||||
get_or_fetch_and_insert_remote_post,
|
||||
get_or_fetch_and_insert_remote_comment, get_or_fetch_and_insert_remote_post,
|
||||
get_or_fetch_and_upsert_remote_user,
|
||||
},
|
||||
ActorType,
|
||||
ApubLikeableType,
|
||||
ApubObjectType,
|
||||
FromApub,
|
||||
ToApub,
|
||||
ActorType, ApubLikeableType, ApubObjectType, FromApub, ToApub,
|
||||
},
|
||||
blocking,
|
||||
convert_datetime,
|
||||
db::{
|
||||
comment::{Comment, CommentForm},
|
||||
community::Community,
|
||||
post::Post,
|
||||
user::User_,
|
||||
Crud,
|
||||
},
|
||||
routes::DbPoolParam,
|
||||
scrape_text_for_mentions,
|
||||
DbPool,
|
||||
LemmyError,
|
||||
MentionData,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
|
||||
@ -40,6 +21,14 @@ use activitystreams::{
|
||||
use activitystreams_new::object::Tombstone;
|
||||
use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
|
||||
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 serde::Deserialize;
|
||||
|
||||
@ -123,7 +112,7 @@ impl FromApub for CommentForm {
|
||||
|
||||
/// Parse an ActivityPub note received from another instance into a Lemmy comment
|
||||
async fn from_apub(
|
||||
note: &mut Note,
|
||||
note: &Note,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<CommentForm, LemmyError> {
|
||||
|
@ -1,35 +1,18 @@
|
||||
use crate::{
|
||||
apub::{
|
||||
activities::{populate_object_props, send_activity},
|
||||
create_apub_response,
|
||||
create_apub_tombstone_response,
|
||||
create_tombstone,
|
||||
create_apub_response, create_apub_tombstone_response, create_tombstone,
|
||||
extensions::group_extensions::GroupExtension,
|
||||
fetcher::get_or_fetch_and_upsert_remote_user,
|
||||
get_shared_inbox,
|
||||
ActorType,
|
||||
FromApub,
|
||||
GroupExt,
|
||||
ToApub,
|
||||
get_shared_inbox, insert_activity, ActorType, FromApub, GroupExt, ToApub,
|
||||
},
|
||||
blocking,
|
||||
convert_datetime,
|
||||
db::{
|
||||
activity::insert_activity,
|
||||
community::{Community, CommunityForm},
|
||||
community_view::{CommunityFollowerView, CommunityModeratorView},
|
||||
user::User_,
|
||||
},
|
||||
naive_now,
|
||||
routes::DbPoolParam,
|
||||
DbPool,
|
||||
LemmyError,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Accept, Announce, Delete, Remove, Undo},
|
||||
Activity,
|
||||
Base,
|
||||
BaseBox,
|
||||
Activity, Base, BaseBox,
|
||||
};
|
||||
use activitystreams_ext::Ext2;
|
||||
use activitystreams_new::{
|
||||
@ -44,6 +27,13 @@ use activitystreams_new::{
|
||||
};
|
||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
||||
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 std::{fmt::Debug, str::FromStr};
|
||||
|
||||
@ -367,13 +357,8 @@ impl FromApub for CommunityForm {
|
||||
type ApubType = GroupExt;
|
||||
|
||||
/// Parse an ActivityPub group received from another instance into a Lemmy community.
|
||||
async fn from_apub(
|
||||
group: &mut GroupExt,
|
||||
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();
|
||||
async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
|
||||
let creator_and_moderator_uris = group.attributed_to().unwrap();
|
||||
let creator_uri = creator_and_moderator_uris
|
||||
.as_many()
|
||||
.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?;
|
||||
|
||||
Ok(CommunityForm {
|
||||
name: group
|
||||
.take_name()
|
||||
.unwrap()
|
||||
.as_single_xsd_string()
|
||||
.unwrap()
|
||||
.into(),
|
||||
title: group.inner.take_preferred_username().unwrap(),
|
||||
name: group.name().unwrap().as_single_xsd_string().unwrap().into(),
|
||||
title: group.inner.preferred_username().unwrap().to_string(),
|
||||
// TODO: should be parsed as html and tags like <script> removed (or use markdown source)
|
||||
// -> same for post.content etc
|
||||
description: group
|
||||
.take_content()
|
||||
.content()
|
||||
.map(|s| s.as_single_xsd_string().unwrap().into()),
|
||||
category_id: group.ext_one.category.identifier.parse::<i32>()?,
|
||||
creator_id: creator.id,
|
||||
removed: None,
|
||||
published: group
|
||||
.take_published()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
updated: group
|
||||
.take_updated()
|
||||
.published()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
updated: group.updated().map(|u| u.as_ref().to_owned().naive_local()),
|
||||
deleted: None,
|
||||
nsfw: group.ext_one.sensitive,
|
||||
actor_id: group.id().unwrap().to_string(),
|
||||
@ -462,39 +440,37 @@ pub async fn get_apub_community_followers(
|
||||
Ok(create_apub_response(&collection))
|
||||
}
|
||||
|
||||
impl Community {
|
||||
pub async fn do_announce<A>(
|
||||
activity: A,
|
||||
community: &Community,
|
||||
sender: &dyn ActorType,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<HttpResponse, LemmyError>
|
||||
where
|
||||
A: Activity + Base + Serialize + Debug,
|
||||
{
|
||||
let mut announce = Announce::default();
|
||||
populate_object_props(
|
||||
&mut announce.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
|
||||
)?;
|
||||
announce
|
||||
.announce_props
|
||||
.set_actor_xsd_any_uri(community.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(activity)?)?;
|
||||
pub async fn do_announce<A>(
|
||||
activity: A,
|
||||
community: &Community,
|
||||
sender: &dyn ActorType,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<HttpResponse, LemmyError>
|
||||
where
|
||||
A: Activity + Base + Serialize + Debug,
|
||||
{
|
||||
let mut announce = Announce::default();
|
||||
populate_object_props(
|
||||
&mut announce.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
|
||||
)?;
|
||||
announce
|
||||
.announce_props
|
||||
.set_actor_xsd_any_uri(community.actor_id.to_owned())?
|
||||
.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
|
||||
// in a database error (same data inserted twice)
|
||||
let mut to = community.get_follower_inboxes(pool).await?;
|
||||
// dont send to the instance where the activity originally came from, because that would result
|
||||
// in a database error (same data inserted twice)
|
||||
let mut to = community.get_follower_inboxes(pool).await?;
|
||||
|
||||
// this seems to be the "easiest" stable alternative for remove_item()
|
||||
to.retain(|x| *x != sender.get_shared_inbox_url());
|
||||
// this seems to be the "easiest" stable alternative for remove_item()
|
||||
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::{
|
||||
extensions::signatures::verify,
|
||||
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
|
||||
insert_activity,
|
||||
ActorType,
|
||||
},
|
||||
blocking,
|
||||
db::{
|
||||
activity::insert_activity,
|
||||
community::{Community, CommunityFollower, CommunityFollowerForm},
|
||||
user::User_,
|
||||
Followable,
|
||||
},
|
||||
routes::{ChatServerParam, DbPoolParam},
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::activity::Undo;
|
||||
use activitystreams_new::activity::Follow;
|
||||
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
||||
use lemmy_db::{
|
||||
community::{Community, CommunityFollower, CommunityFollowerForm},
|
||||
user::User_,
|
||||
Followable,
|
||||
};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Debug;
|
||||
|
@ -1,9 +1,7 @@
|
||||
use crate::{
|
||||
db::{category::Category, Crud},
|
||||
LemmyError,
|
||||
};
|
||||
use crate::LemmyError;
|
||||
use activitystreams::{ext::Extension, Actor};
|
||||
use diesel::PgConnection;
|
||||
use lemmy_db::{category::Category, Crud};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
|
@ -9,7 +9,6 @@ use log::debug;
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::PKey,
|
||||
rsa::Rsa,
|
||||
sign::{Signer, Verifier},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -19,23 +18,6 @@ lazy_static! {
|
||||
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.
|
||||
pub async fn sign(
|
||||
request: ClientRequest,
|
||||
|
@ -1,39 +1,29 @@
|
||||
use crate::{
|
||||
api::site::SearchResponse,
|
||||
apub::{
|
||||
get_apub_protocol_string,
|
||||
is_apub_id_valid,
|
||||
FromApub,
|
||||
GroupExt,
|
||||
PageExt,
|
||||
PersonExt,
|
||||
APUB_JSON_CONTENT_TYPE,
|
||||
},
|
||||
apub::{is_apub_id_valid, FromApub, GroupExt, PageExt, PersonExt, APUB_JSON_CONTENT_TYPE},
|
||||
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},
|
||||
routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
|
||||
DbPool,
|
||||
LemmyError,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::object::Note;
|
||||
use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri};
|
||||
use actix_web::client::Client;
|
||||
use chrono::NaiveDateTime;
|
||||
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 serde::Deserialize;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
@ -171,15 +161,15 @@ pub async fn search_by_apub_id(
|
||||
|
||||
response
|
||||
}
|
||||
SearchAcceptedObjects::Page(mut p) => {
|
||||
let post_form = PostForm::from_apub(&mut p, client, pool).await?;
|
||||
SearchAcceptedObjects::Page(p) => {
|
||||
let post_form = PostForm::from_apub(&p, client, pool).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
|
||||
}
|
||||
SearchAcceptedObjects::Comment(mut c) => {
|
||||
SearchAcceptedObjects::Comment(c) => {
|
||||
let post_url = c
|
||||
.object_props
|
||||
.get_many_in_reply_to_xsd_any_uris()
|
||||
@ -189,9 +179,9 @@ pub async fn search_by_apub_id(
|
||||
.to_string();
|
||||
|
||||
// TODO: also fetch parent comments if any
|
||||
let mut post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
|
||||
let post_form = PostForm::from_apub(&mut post, client, pool).await?;
|
||||
let comment_form = CommentForm::from_apub(&mut c, client, pool).await?;
|
||||
let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
|
||||
let post_form = PostForm::from_apub(&post, client, pool).await?;
|
||||
let comment_form = CommentForm::from_apub(&c, client, pool).await?;
|
||||
|
||||
blocking(pool, move |conn| upsert_post(&post_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
|
||||
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
|
||||
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());
|
||||
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),
|
||||
Err(NotFound {}) => {
|
||||
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??;
|
||||
|
||||
Ok(user)
|
||||
@ -272,9 +262,9 @@ pub async fn get_or_fetch_and_upsert_remote_community(
|
||||
match community {
|
||||
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
|
||||
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());
|
||||
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),
|
||||
Err(NotFound {}) => {
|
||||
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??;
|
||||
|
||||
// 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
|
||||
.as_many()
|
||||
.unwrap()
|
||||
@ -349,8 +339,8 @@ pub async fn get_or_fetch_and_insert_remote_post(
|
||||
Ok(p) => Ok(p),
|
||||
Err(NotFound {}) => {
|
||||
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_form = PostForm::from_apub(&mut post, client, pool).await?;
|
||||
let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
|
||||
let post_form = PostForm::from_apub(&post, client, pool).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: {}",
|
||||
comment_ap_id
|
||||
);
|
||||
let mut 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 = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
|
||||
let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
|
||||
|
||||
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;
|
||||
|
||||
|
@ -16,14 +16,10 @@ use crate::{
|
||||
page_extension::PageExtension,
|
||||
signatures::{PublicKey, PublicKeyExtension},
|
||||
},
|
||||
convert_datetime,
|
||||
db::user::User_,
|
||||
blocking,
|
||||
request::{retry, RecvError},
|
||||
routes::webfinger::WebFingerResponse,
|
||||
DbPool,
|
||||
LemmyError,
|
||||
MentionData,
|
||||
Settings,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::object::Page;
|
||||
use activitystreams_ext::{Ext1, Ext2};
|
||||
@ -35,6 +31,9 @@ use activitystreams_new::{
|
||||
};
|
||||
use actix_web::{body::Body, client::Client, HttpResponse};
|
||||
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 serde::Serialize;
|
||||
use url::Url;
|
||||
@ -45,14 +44,6 @@ type PageExt = Ext1<Page, PageExtension>;
|
||||
|
||||
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
|
||||
/// headers.
|
||||
fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
|
||||
@ -73,34 +64,6 @@ where
|
||||
.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.
|
||||
fn is_apub_id_valid(apub_id: &Url) -> bool {
|
||||
debug!("Checking {}", apub_id);
|
||||
@ -165,7 +128,7 @@ fn create_tombstone(
|
||||
pub trait FromApub {
|
||||
type ApubType;
|
||||
async fn from_apub(
|
||||
apub: &mut Self::ApubType,
|
||||
apub: &Self::ApubType,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Self, LemmyError>
|
||||
@ -374,3 +337,19 @@ pub async fn fetch_webfinger_url(
|
||||
.to_owned()
|
||||
.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::{
|
||||
apub::{
|
||||
activities::{populate_object_props, send_activity_to_community},
|
||||
create_apub_response,
|
||||
create_apub_tombstone_response,
|
||||
create_tombstone,
|
||||
create_apub_response, create_apub_tombstone_response, create_tombstone,
|
||||
extensions::page_extension::PageExtension,
|
||||
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,
|
||||
convert_datetime,
|
||||
db::{
|
||||
community::Community,
|
||||
post::{Post, PostForm},
|
||||
user::User_,
|
||||
Crud,
|
||||
},
|
||||
routes::DbPoolParam,
|
||||
DbPool,
|
||||
LemmyError,
|
||||
Settings,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
|
||||
@ -36,6 +19,13 @@ use activitystreams::{
|
||||
use activitystreams_ext::Ext1;
|
||||
use activitystreams_new::object::Tombstone;
|
||||
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;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -164,7 +154,7 @@ impl FromApub for PostForm {
|
||||
|
||||
/// Parse an ActivityPub page received from another instance into a Lemmy post.
|
||||
async fn from_apub(
|
||||
page: &mut PageExt,
|
||||
page: &PageExt,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<PostForm, LemmyError> {
|
||||
|
@ -1,22 +1,9 @@
|
||||
use crate::{
|
||||
apub::{
|
||||
activities::send_activity,
|
||||
create_tombstone,
|
||||
fetcher::get_or_fetch_and_upsert_remote_user,
|
||||
ApubObjectType,
|
||||
FromApub,
|
||||
ToApub,
|
||||
activities::send_activity, create_tombstone, fetcher::get_or_fetch_and_upsert_remote_user,
|
||||
insert_activity, ApubObjectType, FromApub, ToApub,
|
||||
},
|
||||
blocking,
|
||||
convert_datetime,
|
||||
db::{
|
||||
activity::insert_activity,
|
||||
private_message::{PrivateMessage, PrivateMessageForm},
|
||||
user::User_,
|
||||
Crud,
|
||||
},
|
||||
DbPool,
|
||||
LemmyError,
|
||||
blocking, DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Create, Delete, Undo, Update},
|
||||
@ -25,6 +12,12 @@ use activitystreams::{
|
||||
};
|
||||
use activitystreams_new::object::Tombstone;
|
||||
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)]
|
||||
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
|
||||
async fn from_apub(
|
||||
note: &mut Note,
|
||||
note: &Note,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<PrivateMessageForm, LemmyError> {
|
||||
|
@ -5,47 +5,39 @@ use crate::{
|
||||
post::PostResponse,
|
||||
},
|
||||
apub::{
|
||||
community::do_announce,
|
||||
extensions::signatures::verify,
|
||||
fetcher::{
|
||||
get_or_fetch_and_insert_remote_comment,
|
||||
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_insert_remote_comment, get_or_fetch_and_insert_remote_post,
|
||||
get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user,
|
||||
},
|
||||
FromApub,
|
||||
GroupExt,
|
||||
PageExt,
|
||||
insert_activity, FromApub, GroupExt, PageExt,
|
||||
},
|
||||
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},
|
||||
scrape_text_for_mentions,
|
||||
websocket::{
|
||||
server::{SendComment, SendCommunityRoomMessage, SendPost},
|
||||
UserOperation,
|
||||
},
|
||||
DbPool,
|
||||
LemmyError,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Announce, Create, Delete, Dislike, Like, Remove, Undo, Update},
|
||||
object::Note,
|
||||
Activity,
|
||||
Base,
|
||||
BaseBox,
|
||||
Activity, Base, BaseBox,
|
||||
};
|
||||
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 serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
@ -234,7 +226,7 @@ where
|
||||
if community.local {
|
||||
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 {
|
||||
Ok(HttpResponse::NotFound().finish())
|
||||
}
|
||||
@ -335,7 +327,7 @@ async fn receive_create_post(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut page = create
|
||||
let page = create
|
||||
.create_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -353,7 +345,7 @@ async fn receive_create_post(
|
||||
|
||||
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??;
|
||||
|
||||
@ -381,7 +373,7 @@ async fn receive_create_comment(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = create
|
||||
let note = create
|
||||
.create_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -399,7 +391,7 @@ async fn receive_create_comment(
|
||||
|
||||
insert_activity(user.id, create, false, pool).await?;
|
||||
|
||||
let comment = CommentForm::from_apub(&mut note, client, pool).await?;
|
||||
let comment = CommentForm::from_apub(¬e, client, pool).await?;
|
||||
|
||||
let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
|
||||
|
||||
@ -440,7 +432,7 @@ async fn receive_update_post(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut page = update
|
||||
let page = update
|
||||
.update_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -458,7 +450,7 @@ async fn receive_update_post(
|
||||
|
||||
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)
|
||||
.await?
|
||||
@ -486,7 +478,7 @@ async fn receive_like_post(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut page = like
|
||||
let page = like
|
||||
.like_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -500,7 +492,7 @@ async fn receive_like_post(
|
||||
|
||||
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)
|
||||
.await?
|
||||
@ -537,7 +529,7 @@ async fn receive_dislike_post(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut page = dislike
|
||||
let page = dislike
|
||||
.dislike_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -555,7 +547,7 @@ async fn receive_dislike_post(
|
||||
|
||||
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)
|
||||
.await?
|
||||
@ -592,7 +584,7 @@ async fn receive_update_comment(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = update
|
||||
let note = update
|
||||
.update_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -610,7 +602,7 @@ async fn receive_update_comment(
|
||||
|
||||
insert_activity(user.id, update, false, pool).await?;
|
||||
|
||||
let comment = CommentForm::from_apub(&mut note, client, pool).await?;
|
||||
let comment = CommentForm::from_apub(¬e, client, pool).await?;
|
||||
|
||||
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
|
||||
.await?
|
||||
@ -651,7 +643,7 @@ async fn receive_like_comment(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = like
|
||||
let note = like
|
||||
.like_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -665,7 +657,7 @@ async fn receive_like_comment(
|
||||
|
||||
insert_activity(user.id, like, false, pool).await?;
|
||||
|
||||
let comment = CommentForm::from_apub(&mut note, client, pool).await?;
|
||||
let comment = CommentForm::from_apub(¬e, client, pool).await?;
|
||||
|
||||
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
|
||||
.await?
|
||||
@ -709,7 +701,7 @@ async fn receive_dislike_comment(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = dislike
|
||||
let note = dislike
|
||||
.dislike_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -727,7 +719,7 @@ async fn receive_dislike_comment(
|
||||
|
||||
insert_activity(user.id, dislike, false, pool).await?;
|
||||
|
||||
let comment = CommentForm::from_apub(&mut note, client, pool).await?;
|
||||
let comment = CommentForm::from_apub(¬e, client, pool).await?;
|
||||
|
||||
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
|
||||
.await?
|
||||
@ -777,7 +769,7 @@ async fn receive_delete_community(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut group = delete
|
||||
let group = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -789,7 +781,7 @@ async fn receive_delete_community(
|
||||
|
||||
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?
|
||||
.actor_id;
|
||||
|
||||
@ -854,7 +846,7 @@ async fn receive_remove_community(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut group = remove
|
||||
let group = remove
|
||||
.remove_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -866,7 +858,7 @@ async fn receive_remove_community(
|
||||
|
||||
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?
|
||||
.actor_id;
|
||||
|
||||
@ -931,7 +923,7 @@ async fn receive_delete_post(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut page = delete
|
||||
let page = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -943,7 +935,7 @@ async fn receive_delete_post(
|
||||
|
||||
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?;
|
||||
|
||||
@ -997,7 +989,7 @@ async fn receive_remove_post(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut page = remove
|
||||
let page = remove
|
||||
.remove_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1009,7 +1001,7 @@ async fn receive_remove_post(
|
||||
|
||||
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?;
|
||||
|
||||
@ -1063,7 +1055,7 @@ async fn receive_delete_comment(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut note = delete
|
||||
let note = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1075,7 +1067,7 @@ async fn receive_delete_comment(
|
||||
|
||||
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(¬e, client, pool).await?.ap_id;
|
||||
|
||||
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()
|
||||
.to_string();
|
||||
|
||||
let mut note = remove
|
||||
let note = remove
|
||||
.remove_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1143,7 +1135,7 @@ async fn receive_remove_comment(
|
||||
|
||||
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(¬e, client, pool).await?.ap_id;
|
||||
|
||||
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()
|
||||
.to_string();
|
||||
|
||||
let mut note = delete
|
||||
let note = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1271,7 +1263,7 @@ async fn receive_undo_delete_comment(
|
||||
|
||||
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(¬e, client, pool).await?.ap_id;
|
||||
|
||||
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()
|
||||
.to_string();
|
||||
|
||||
let mut note = remove
|
||||
let note = remove
|
||||
.remove_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1339,7 +1331,7 @@ async fn receive_undo_remove_comment(
|
||||
|
||||
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(¬e, client, pool).await?.ap_id;
|
||||
|
||||
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()
|
||||
.to_string();
|
||||
|
||||
let mut page = delete
|
||||
let page = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1407,7 +1399,7 @@ async fn receive_undo_delete_post(
|
||||
|
||||
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?;
|
||||
|
||||
@ -1461,7 +1453,7 @@ async fn receive_undo_remove_post(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut page = remove
|
||||
let page = remove
|
||||
.remove_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1473,7 +1465,7 @@ async fn receive_undo_remove_post(
|
||||
|
||||
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?;
|
||||
|
||||
@ -1527,7 +1519,7 @@ async fn receive_undo_delete_community(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut group = delete
|
||||
let group = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1539,7 +1531,7 @@ async fn receive_undo_delete_community(
|
||||
|
||||
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?
|
||||
.actor_id;
|
||||
|
||||
@ -1604,7 +1596,7 @@ async fn receive_undo_remove_community(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut group = remove
|
||||
let group = remove
|
||||
.remove_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1616,7 +1608,7 @@ async fn receive_undo_remove_community(
|
||||
|
||||
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?
|
||||
.actor_id;
|
||||
|
||||
@ -1704,7 +1696,7 @@ async fn receive_undo_like_comment(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = like
|
||||
let note = like
|
||||
.like_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1718,7 +1710,7 @@ async fn receive_undo_like_comment(
|
||||
|
||||
insert_activity(user.id, like, false, pool).await?;
|
||||
|
||||
let comment = CommentForm::from_apub(&mut note, client, pool).await?;
|
||||
let comment = CommentForm::from_apub(¬e, client, pool).await?;
|
||||
|
||||
let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
|
||||
.await?
|
||||
@ -1758,7 +1750,7 @@ async fn receive_undo_like_post(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut page = like
|
||||
let page = like
|
||||
.like_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -1772,7 +1764,7 @@ async fn receive_undo_like_post(
|
||||
|
||||
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)
|
||||
.await?
|
||||
|
@ -1,15 +1,12 @@
|
||||
use crate::{
|
||||
apub::{activities::send_activity, create_apub_response, ActorType, FromApub, PersonExt, ToApub},
|
||||
blocking,
|
||||
convert_datetime,
|
||||
db::{
|
||||
activity::insert_activity,
|
||||
user::{UserForm, User_},
|
||||
api::claims::Claims,
|
||||
apub::{
|
||||
activities::send_activity, create_apub_response, insert_activity, ActorType, FromApub,
|
||||
PersonExt, ToApub,
|
||||
},
|
||||
naive_now,
|
||||
blocking,
|
||||
routes::DbPoolParam,
|
||||
DbPool,
|
||||
LemmyError,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams_ext::Ext1;
|
||||
use activitystreams_new::{
|
||||
@ -22,6 +19,11 @@ use activitystreams_new::{
|
||||
};
|
||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
||||
use failure::_core::str::FromStr;
|
||||
use lemmy_db::{
|
||||
naive_now,
|
||||
user::{UserForm, User_},
|
||||
};
|
||||
use lemmy_utils::convert_datetime;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -185,8 +187,8 @@ impl ActorType for User_ {
|
||||
impl FromApub for UserForm {
|
||||
type ApubType = PersonExt;
|
||||
/// Parse an ActivityPub person received from another instance into a Lemmy user.
|
||||
async fn from_apub(person: &mut PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
|
||||
let avatar = match person.take_icon() {
|
||||
async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
|
||||
let avatar = match person.icon() {
|
||||
Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
@ -199,19 +201,19 @@ impl FromApub for UserForm {
|
||||
|
||||
Ok(UserForm {
|
||||
name: person
|
||||
.take_name()
|
||||
.name()
|
||||
.unwrap()
|
||||
.as_single_xsd_string()
|
||||
.unwrap()
|
||||
.into(),
|
||||
preferred_username: person.inner.take_preferred_username(),
|
||||
preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
|
||||
password_encrypted: "".to_string(),
|
||||
admin: false,
|
||||
banned: false,
|
||||
email: None,
|
||||
avatar,
|
||||
updated: person
|
||||
.take_updated()
|
||||
.updated()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
show_nsfw: false,
|
||||
theme: "".to_string(),
|
||||
@ -223,7 +225,7 @@ impl FromApub for UserForm {
|
||||
matrix_user_id: None,
|
||||
actor_id: person.id().unwrap().to_string(),
|
||||
bio: person
|
||||
.take_summary()
|
||||
.summary()
|
||||
.map(|s| s.as_single_xsd_string().unwrap().into()),
|
||||
local: false,
|
||||
private_key: None,
|
||||
@ -240,7 +242,7 @@ pub async fn get_apub_user_http(
|
||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
||||
let user_name = info.into_inner().user_name;
|
||||
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??;
|
||||
let u = user.to_apub(&db).await?;
|
||||
|
@ -3,29 +3,26 @@ use crate::{
|
||||
apub::{
|
||||
extensions::signatures::verify,
|
||||
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
|
||||
FromApub,
|
||||
insert_activity, FromApub,
|
||||
},
|
||||
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},
|
||||
websocket::{server::SendUserRoomMessage, UserOperation},
|
||||
DbPool,
|
||||
LemmyError,
|
||||
DbPool, LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Accept, Create, Delete, Undo, Update},
|
||||
object::Note,
|
||||
};
|
||||
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 serde::Deserialize;
|
||||
use std::fmt::Debug;
|
||||
@ -116,7 +113,7 @@ async fn receive_create_private_message(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = create
|
||||
let note = create
|
||||
.create_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -135,7 +132,7 @@ async fn receive_create_private_message(
|
||||
|
||||
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(¬e, client, pool).await?;
|
||||
|
||||
let inserted_private_message = blocking(pool, move |conn| {
|
||||
PrivateMessage::create(conn, &private_message)
|
||||
@ -168,7 +165,7 @@ async fn receive_update_private_message(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = update
|
||||
let note = update
|
||||
.update_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -187,7 +184,7 @@ async fn receive_update_private_message(
|
||||
|
||||
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(¬e, client, pool).await?;
|
||||
|
||||
let private_message_ap_id = private_message_form.ap_id.clone();
|
||||
let private_message = blocking(pool, move |conn| {
|
||||
@ -228,7 +225,7 @@ async fn receive_delete_private_message(
|
||||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let mut note = delete
|
||||
let note = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -247,7 +244,7 @@ async fn receive_delete_private_message(
|
||||
|
||||
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(¬e, client, pool).await?;
|
||||
|
||||
let private_message_ap_id = private_message_form.ap_id;
|
||||
let private_message = blocking(pool, move |conn| {
|
||||
@ -308,7 +305,7 @@ async fn receive_undo_delete_private_message(
|
||||
.to_owned()
|
||||
.into_concrete::<Delete>()?;
|
||||
|
||||
let mut note = delete
|
||||
let note = delete
|
||||
.delete_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
@ -327,7 +324,7 @@ async fn receive_undo_delete_private_message(
|
||||
|
||||
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(¬e, client, pool).await?;
|
||||
|
||||
let private_message_ap_id = private_message.ap_id.clone();
|
||||
let private_message_id = blocking(pool, move |conn| {
|
||||
|
@ -1,18 +1,16 @@
|
||||
// This is for db migrations that require code
|
||||
use super::{
|
||||
use crate::LemmyError;
|
||||
use diesel::*;
|
||||
use lemmy_db::{
|
||||
comment::Comment,
|
||||
community::{Community, CommunityForm},
|
||||
naive_now,
|
||||
post::Post,
|
||||
private_message::PrivateMessage,
|
||||
user::{UserForm, User_},
|
||||
Crud,
|
||||
};
|
||||
use crate::{
|
||||
apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
|
||||
db::Crud,
|
||||
naive_now,
|
||||
LemmyError,
|
||||
};
|
||||
use diesel::*;
|
||||
use lemmy_utils::{generate_actor_keypair, make_apub_endpoint, EndpointType};
|
||||
use log::info;
|
||||
|
||||
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> {
|
||||
use crate::schema::user_::dsl::*;
|
||||
use lemmy_db::schema::user_::dsl::*;
|
||||
|
||||
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> {
|
||||
use crate::schema::community::dsl::*;
|
||||
use lemmy_db::schema::community::dsl::*;
|
||||
|
||||
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> {
|
||||
use crate::schema::post::dsl::*;
|
||||
use lemmy_db::schema::post::dsl::*;
|
||||
|
||||
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)?;
|
||||
|
||||
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());
|
||||
@ -145,7 +144,7 @@ fn post_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");
|
||||
|
||||
@ -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)?;
|
||||
|
||||
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)?;
|
||||
@ -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> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
use lemmy_db::schema::private_message::dsl::*;
|
||||
|
||||
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)?;
|
||||
|
||||
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());
|
@ -5,76 +5,34 @@ pub extern crate strum_macros;
|
||||
pub extern crate lazy_static;
|
||||
#[macro_use]
|
||||
pub extern crate failure;
|
||||
#[macro_use]
|
||||
pub extern crate diesel;
|
||||
pub extern crate actix;
|
||||
pub extern crate actix_web;
|
||||
pub extern crate bcrypt;
|
||||
pub extern crate chrono;
|
||||
pub extern crate comrak;
|
||||
pub extern crate diesel;
|
||||
pub extern crate dotenv;
|
||||
pub extern crate jsonwebtoken;
|
||||
pub extern crate lettre;
|
||||
pub extern crate lettre_email;
|
||||
extern crate log;
|
||||
pub extern crate openssl;
|
||||
pub extern crate rand;
|
||||
pub extern crate regex;
|
||||
pub extern crate rss;
|
||||
pub extern crate serde;
|
||||
pub extern crate serde_json;
|
||||
pub extern crate sha2;
|
||||
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 apub;
|
||||
pub mod db;
|
||||
pub mod code_migrations;
|
||||
pub mod rate_limit;
|
||||
pub mod request;
|
||||
pub mod routes;
|
||||
pub mod schema;
|
||||
pub mod settings;
|
||||
pub mod version;
|
||||
pub mod websocket;
|
||||
|
||||
use crate::{
|
||||
request::{retry, RecvError},
|
||||
settings::Settings,
|
||||
};
|
||||
use crate::request::{retry, RecvError};
|
||||
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 percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
|
||||
@ -89,14 +47,6 @@ pub struct LemmyError {
|
||||
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
|
||||
where
|
||||
T: Into<failure::Error>,
|
||||
@ -106,113 +56,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||
DateTime::<Utc>::from_utc(ndt, Utc)
|
||||
}
|
||||
|
||||
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())
|
||||
impl std::fmt::Display for LemmyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
impl actix_web::error::ResponseError for LemmyError {}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct IframelyResponse {
|
||||
@ -319,8 +169,20 @@ async fn fetch_iframely_and_pictrs_data(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn markdown_to_html(text: &str) -> String {
|
||||
comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
|
||||
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 get_ip(conn_info: &ConnectionInfo) -> String {
|
||||
@ -333,127 +195,37 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
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?;
|
||||
|
||||
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 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 {
|
||||
VALID_COMMUNITY_NAME_REGEX.is_match(name)
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
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());
|
||||
}
|
||||
use crate::is_image_content_type;
|
||||
|
||||
#[test]
|
||||
fn test_image() {
|
||||
actix_rt::System::new("tset_image").block_on(async move {
|
||||
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://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
|
||||
)
|
||||
.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);
|
||||
}
|
||||
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://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
|
||||
)
|
||||
.await.is_err()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 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");
|
||||
// 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},
|
||||
PgConnection,
|
||||
};
|
||||
use lemmy_db::get_database_url_from_env;
|
||||
use lemmy_server::{
|
||||
blocking,
|
||||
db::code_migrations::run_advanced_migrations,
|
||||
code_migrations::run_advanced_migrations,
|
||||
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
||||
routes::{api, federation, feeds, index, nodeinfo, webfinger},
|
||||
settings::Settings,
|
||||
websocket::server::*,
|
||||
LemmyError,
|
||||
};
|
||||
use regex::Regex;
|
||||
use lemmy_utils::{settings::Settings, CACHE_CONTROL_REGEX};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
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);
|
||||
// Test out 1 hour here, this is breaking some things
|
||||
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();
|
||||
|
||||
// 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()
|
||||
.max_size(settings.database.pool_size)
|
||||
.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
|
||||
blocking(&pool, move |conn| {
|
||||
|
@ -1,7 +1,8 @@
|
||||
use super::{IPAddr, Settings};
|
||||
use crate::{get_ip, settings::RateLimitConfig, LemmyError};
|
||||
use super::IPAddr;
|
||||
use crate::{get_ip, LemmyError};
|
||||
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use futures::future::{ok, Ready};
|
||||
use lemmy_utils::settings::{RateLimitConfig, Settings};
|
||||
use rate_limiter::{RateLimitType, RateLimiter};
|
||||
use std::{
|
||||
future::Future,
|
||||
|
@ -1,21 +1,23 @@
|
||||
use crate::{
|
||||
apub::{
|
||||
comment::get_apub_comment,
|
||||
community::*,
|
||||
community_inbox::community_inbox,
|
||||
post::get_apub_post,
|
||||
shared_inbox::shared_inbox,
|
||||
user::*,
|
||||
user_inbox::user_inbox,
|
||||
APUB_JSON_CONTENT_TYPE,
|
||||
},
|
||||
settings::Settings,
|
||||
use crate::apub::{
|
||||
comment::get_apub_comment,
|
||||
community::*,
|
||||
community_inbox::community_inbox,
|
||||
post::get_apub_post,
|
||||
shared_inbox::shared_inbox,
|
||||
user::*,
|
||||
user_inbox::user_inbox,
|
||||
APUB_JSON_CONTENT_TYPE,
|
||||
};
|
||||
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) {
|
||||
if Settings::get().federation.enabled {
|
||||
println!("federation enabled, host is {}", Settings::get().hostname);
|
||||
let digest_verifier = VerifyDigest::new(Sha256::new());
|
||||
|
||||
cfg
|
||||
.service(
|
||||
web::scope("/")
|
||||
@ -38,8 +40,20 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.route("/comment/{comment_id}", web::get().to(get_apub_comment)),
|
||||
)
|
||||
// Inboxes dont work with the header guard for some reason.
|
||||
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
|
||||
.route("/u/{user_name}/inbox", web::post().to(user_inbox))
|
||||
.route("/inbox", web::post().to(shared_inbox));
|
||||
.service(
|
||||
web::resource("/c/{community_name}/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::{
|
||||
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 crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError};
|
||||
use actix_web::{error::ErrorBadRequest, *};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use diesel::{
|
||||
r2d2::{ConnectionManager, Pool},
|
||||
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 serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
@ -131,7 +126,7 @@ fn get_feed_user(
|
||||
) -> Result<ChannelBuilder, LemmyError> {
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
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)
|
||||
.listing_type(ListingType::All)
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::settings::Settings;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::*;
|
||||
use lemmy_utils::settings::Settings;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
|
@ -1,13 +1,7 @@
|
||||
use crate::{
|
||||
apub::get_apub_protocol_string,
|
||||
blocking,
|
||||
db::site_view::SiteView,
|
||||
routes::DbPoolParam,
|
||||
version,
|
||||
LemmyError,
|
||||
Settings,
|
||||
};
|
||||
use crate::{blocking, routes::DbPoolParam, version, LemmyError};
|
||||
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 url::Url;
|
||||
|
||||
|
@ -1,12 +1,7 @@
|
||||
use crate::{
|
||||
blocking,
|
||||
db::{community::Community, user::User_},
|
||||
routes::DbPoolParam,
|
||||
LemmyError,
|
||||
Settings,
|
||||
};
|
||||
use crate::{blocking, routes::DbPoolParam, LemmyError};
|
||||
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};
|
||||
|
||||
#[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
|
||||
/// this, but it described in this blog post:
|
||||
/// 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
Executable file
5
server/test.sh
vendored
Executable file
@ -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
|
1
ui/assets/css/choices.min.css
vendored
Normal file
1
ui/assets/css/choices.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
ui/assets/css/main.css
vendored
7
ui/assets/css/main.css
vendored
@ -264,3 +264,10 @@ pre {
|
||||
width: 0px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
br.big {
|
||||
display: block;
|
||||
content: "";
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
7
ui/assets/css/selectr.min.css
vendored
7
ui/assets/css/selectr.min.css
vendored
File diff suppressed because one or more lines are too long
8
ui/fuse.js
vendored
8
ui/fuse.js
vendored
@ -6,12 +6,10 @@ const {
|
||||
WebIndexPlugin,
|
||||
QuantumPlugin,
|
||||
} = require('fuse-box');
|
||||
// const transformInferno = require('../../dist').default
|
||||
const transformInferno = require('ts-transform-inferno').default;
|
||||
const transformClasscat = require('ts-transform-classcat').default;
|
||||
let fuse, app;
|
||||
let isProduction = false;
|
||||
// var setVersion = require('./set_version.js').setVersion;
|
||||
|
||||
Sparky.task('config', _ => {
|
||||
fuse = new FuseBox({
|
||||
@ -45,18 +43,18 @@ Sparky.task('config', _ => {
|
||||
});
|
||||
app = fuse.bundle('app').instructions('>index.tsx');
|
||||
});
|
||||
// Sparky.task('version', _ => setVersion());
|
||||
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
||||
Sparky.task('env', _ => (isProduction = true));
|
||||
Sparky.task('copy-assets', () =>
|
||||
Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
|
||||
);
|
||||
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
|
||||
fuse.dev();
|
||||
fuse.dev({
|
||||
fallback: 'index.html',
|
||||
});
|
||||
app.hmr().watch();
|
||||
return fuse.run();
|
||||
});
|
||||
Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => {
|
||||
// fuse.dev({ reload: true }); // remove after demo
|
||||
return fuse.run();
|
||||
});
|
||||
|
6
ui/package.json
vendored
6
ui/package.json
vendored
@ -15,7 +15,6 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"@joeattardi/emoji-button": "^2.12.1",
|
||||
"@types/autosize": "^3.0.6",
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
@ -24,6 +23,7 @@
|
||||
"@types/node": "^13.11.1",
|
||||
"autosize": "^4.0.2",
|
||||
"bootswatch": "^4.3.1",
|
||||
"choices.js": "^9.0.1",
|
||||
"classcat": "^4.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"emoji-short-name": "^1.0.0",
|
||||
@ -37,7 +37,6 @@
|
||||
"markdown-it": "^10.0.0",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"mobius1-selectr": "^2.4.13",
|
||||
"moment": "^2.24.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"prettier": "^2.0.4",
|
||||
@ -47,7 +46,6 @@
|
||||
"tippy.js": "^6.1.1",
|
||||
"toastify-js": "^1.7.0",
|
||||
"tributejs": "^5.1.3",
|
||||
"twemoji": "^12.1.2",
|
||||
"ws": "^7.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -72,7 +70,7 @@
|
||||
"engineStrict": true,
|
||||
"husky": {
|
||||
"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": {
|
||||
|
25
ui/src/components/cake-day.tsx
vendored
Normal file
25
ui/src/components/cake-day.tsx
vendored
Normal file
@ -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 });
|
||||
}
|
||||
}
|
265
ui/src/components/comment-form.tsx
vendored
265
ui/src/components/comment-form.tsx
vendored
@ -1,4 +1,5 @@
|
||||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { Prompt } from 'inferno-router';
|
||||
@ -17,7 +18,6 @@ import {
|
||||
toast,
|
||||
setupTribute,
|
||||
wsJsonToRes,
|
||||
emojiPicker,
|
||||
pictrsDeleteToast,
|
||||
} from '../utils';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
@ -25,6 +25,7 @@ import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import emojiShortName from 'emoji-short-name';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
interface CommentFormProps {
|
||||
postId?: number;
|
||||
@ -32,6 +33,7 @@ interface CommentFormProps {
|
||||
onReplyCancel?(): any;
|
||||
edit?: boolean;
|
||||
disabled?: boolean;
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
interface CommentFormState {
|
||||
@ -72,7 +74,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.setupEmojiPicker();
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
@ -98,14 +99,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var textarea: any = document.getElementById(this.id);
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.commentForm.content = textarea.value;
|
||||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
if (textarea) {
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.commentForm.content = textarea.value;
|
||||
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() {
|
||||
@ -128,133 +149,123 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
when={this.state.commentForm.content}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
<form
|
||||
id={this.formId}
|
||||
onSubmit={linkEvent(this, this.handleCommentSubmit)}
|
||||
>
|
||||
<div class="form-group row">
|
||||
<div className={`col-sm-12`}>
|
||||
<textarea
|
||||
id={this.id}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
value={this.state.commentForm.content}
|
||||
onInput={linkEvent(this, this.handleCommentContentChange)}
|
||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
||||
required
|
||||
disabled={this.props.disabled}
|
||||
rows={2}
|
||||
maxLength={10000}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="card card-body md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.commentForm.content
|
||||
)}
|
||||
{UserService.Instance.user ? (
|
||||
<form
|
||||
id={this.formId}
|
||||
onSubmit={linkEvent(this, this.handleCommentSubmit)}
|
||||
>
|
||||
<div class="form-group row">
|
||||
<div className={`col-sm-12`}>
|
||||
<textarea
|
||||
id={this.id}
|
||||
className={`form-control ${
|
||||
this.state.previewMode && 'd-none'
|
||||
}`}
|
||||
value={this.state.commentForm.content}
|
||||
onInput={linkEvent(this, this.handleCommentContentChange)}
|
||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
||||
required
|
||||
disabled={this.props.disabled}
|
||||
rows={2}
|
||||
maxLength={10000}
|
||||
/>
|
||||
)}
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="card card-body md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.commentForm.content
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
disabled={this.props.disabled || this.state.loading}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
{this.state.commentForm.content && (
|
||||
<button
|
||||
className={`btn btn-sm mr-2 btn-secondary ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
{this.props.node && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
onClick={linkEvent(this, this.handleReplyCancel)}
|
||||
>
|
||||
{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">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
|
||||
<label
|
||||
htmlFor={`file-upload-${this.id}`}
|
||||
className={`${UserService.Instance.user && 'pointer'}`}
|
||||
data-tippy-content={i18n.t('upload_image')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-image"></use>
|
||||
</svg>
|
||||
</label>
|
||||
<input
|
||||
id={`file-upload-${this.id}`}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
name="file"
|
||||
class="d-none"
|
||||
disabled={!UserService.Instance.user}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
{this.state.imageLoading && (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<span>{this.state.buttonTitle}</span>
|
||||
)}
|
||||
</button>
|
||||
{this.state.commentForm.content && (
|
||||
<button
|
||||
className={`btn btn-sm mr-2 btn-secondary ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
{this.props.node && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
onClick={linkEvent(this, this.handleReplyCancel)}
|
||||
>
|
||||
{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">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
|
||||
<label
|
||||
htmlFor={`file-upload-${this.id}`}
|
||||
className={`${UserService.Instance.user && 'pointer'}`}
|
||||
data-tippy-content={i18n.t('upload_image')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-image"></use>
|
||||
</svg>
|
||||
</label>
|
||||
<input
|
||||
id={`file-upload-${this.id}`}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
name="file"
|
||||
class="d-none"
|
||||
disabled={!UserService.Instance.user}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
{this.state.imageLoading && (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
)}
|
||||
<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>
|
||||
</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>
|
||||
</form>
|
||||
)}
|
||||
</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) {
|
||||
let isReply =
|
||||
this.props.node !== undefined && data.comment.parent_id !== null;
|
||||
@ -302,10 +313,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleEmojiPickerClick(_i: CommentForm, event: any) {
|
||||
emojiPicker.togglePicker(event.target);
|
||||
}
|
||||
|
||||
handleCommentContentChange(i: CommentForm, event: any) {
|
||||
i.state.commentForm.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
|
32
ui/src/components/comment-node.tsx
vendored
32
ui/src/components/comment-node.tsx
vendored
@ -32,6 +32,7 @@ import { MomentTime } from './moment-time';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { UserListing } from './user-listing';
|
||||
import { CommunityLink } from './community-link';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface CommentNodeState {
|
||||
@ -158,9 +159,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
id: node.comment.creator_id,
|
||||
local: node.comment.creator_local,
|
||||
actor_id: node.comment.creator_actor_id,
|
||||
published: node.comment.creator_published,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{this.isMod && (
|
||||
<div className="badge badge-light d-none d-sm-inline mr-2">
|
||||
{i18n.t('mod')}
|
||||
@ -184,13 +187,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
{this.props.showCommunity && (
|
||||
<>
|
||||
<span class="mx-1">{i18n.t('to')}</span>
|
||||
<Link class="mr-2" to={`/c/${node.comment.community_name}`}>
|
||||
{node.comment.community_name}
|
||||
<CommunityLink
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
onClick={linkEvent(this, this.handleCommentCollapse)}
|
||||
>
|
||||
{this.state.collapsed ? (
|
||||
@ -202,9 +214,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
<use xlinkHref="#icon-minus-square"></use>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`unselectable pointer ${this.scoreColor}`}
|
||||
</button>
|
||||
{/* This is an expanding spacer for mobile */}
|
||||
<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)}
|
||||
data-tippy-content={this.pointsTippy}
|
||||
>
|
||||
@ -212,7 +226,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
<use xlinkHref="#icon-zap"></use>
|
||||
</svg>
|
||||
<span class="mr-1">{this.state.score}</span>
|
||||
</span>
|
||||
</button>
|
||||
<span className="mr-1">•</span>
|
||||
<span>
|
||||
<MomentTime data={node.comment} />
|
||||
@ -225,6 +239,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
edit
|
||||
onReplyCancel={this.handleReplyCancel}
|
||||
disabled={this.props.locked}
|
||||
focus
|
||||
/>
|
||||
)}
|
||||
{!this.state.showEdit && !this.state.collapsed && (
|
||||
@ -693,6 +708,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
node={node}
|
||||
onReplyCancel={this.handleReplyCancel}
|
||||
disabled={this.props.locked}
|
||||
focus
|
||||
/>
|
||||
)}
|
||||
{node.children && !this.state.collapsed && (
|
||||
|
2
ui/src/components/communities.tsx
vendored
2
ui/src/components/communities.tsx
vendored
@ -160,7 +160,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{this.state.communities.length == communityLimit && (
|
||||
{this.state.communities.length > 0 && (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
|
2
ui/src/components/community.tsx
vendored
2
ui/src/components/community.tsx
vendored
@ -260,7 +260,7 @@ export class Community extends Component<any, State> {
|
||||
{i18n.t('prev')}
|
||||
</button>
|
||||
)}
|
||||
{this.state.posts.length == fetchLimit && (
|
||||
{this.state.posts.length > 0 && (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
|
7
ui/src/components/create-community.tsx
vendored
7
ui/src/components/create-community.tsx
vendored
@ -9,7 +9,7 @@ import {
|
||||
GetSiteResponse,
|
||||
} from '../interfaces';
|
||||
import { toast, wsJsonToRes } from '../utils';
|
||||
import { WebSocketService } from '../services';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface CreateCommunityState {
|
||||
@ -26,6 +26,11 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
|
||||
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
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
|
7
ui/src/components/create-post.tsx
vendored
7
ui/src/components/create-post.tsx
vendored
@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { PostForm } from './post-form';
|
||||
import { toast, wsJsonToRes } from '../utils';
|
||||
import { WebSocketService } from '../services';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import {
|
||||
UserOperation,
|
||||
PostFormParams,
|
||||
@ -41,6 +41,11 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||
this.handlePostCreate = this.handlePostCreate.bind(this);
|
||||
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
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
|
7
ui/src/components/create-private-message.tsx
vendored
7
ui/src/components/create-private-message.tsx
vendored
@ -2,7 +2,7 @@ import { Component } from 'inferno';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { PrivateMessageForm } from './private-message-form';
|
||||
import { WebSocketService } from '../services';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import {
|
||||
UserOperation,
|
||||
WebSocketJsonResponse,
|
||||
@ -20,6 +20,11 @@ export class CreatePrivateMessage extends Component<any, any> {
|
||||
this
|
||||
);
|
||||
|
||||
if (!UserService.Instance.user) {
|
||||
toast(i18n.t('not_logged_in'), 'danger');
|
||||
this.context.router.history.push(`/login`);
|
||||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
|
35
ui/src/components/inbox.tsx
vendored
35
ui/src/components/inbox.tsx
vendored
@ -267,6 +267,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||
nodes={[{ comment: i }]}
|
||||
noIndent
|
||||
markable
|
||||
showCommunity
|
||||
showContext
|
||||
enableDownvotes={this.state.enableDownvotes}
|
||||
/>
|
||||
@ -285,6 +286,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||
nodes={commentsToFlatNodes(this.state.replies)}
|
||||
noIndent
|
||||
markable
|
||||
showCommunity
|
||||
showContext
|
||||
enableDownvotes={this.state.enableDownvotes}
|
||||
/>
|
||||
@ -300,6 +302,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||
nodes={[{ comment: mention }]}
|
||||
noIndent
|
||||
markable
|
||||
showCommunity
|
||||
showContext
|
||||
enableDownvotes={this.state.enableDownvotes}
|
||||
/>
|
||||
@ -329,12 +332,14 @@ export class Inbox extends Component<any, InboxState> {
|
||||
{i18n.t('prev')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
{i18n.t('next')}
|
||||
</button>
|
||||
{this.unreadCount() > 0 && (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
{i18n.t('next')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -534,15 +539,19 @@ export class Inbox extends Component<any, InboxState> {
|
||||
}
|
||||
|
||||
sendUnreadCount() {
|
||||
let count =
|
||||
this.state.replies.filter(r => !r.read).length +
|
||||
this.state.mentions.filter(r => !r.read).length +
|
||||
this.state.messages.filter(
|
||||
r => !r.read && r.creator_id !== UserService.Instance.user.id
|
||||
).length;
|
||||
UserService.Instance.user.unreadCount = 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.mentions.filter(r => !r.read).length +
|
||||
this.state.messages.filter(
|
||||
r => !r.read && r.creator_id !== UserService.Instance.user.id
|
||||
).length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
12
ui/src/components/main.tsx
vendored
12
ui/src/components/main.tsx
vendored
@ -373,17 +373,21 @@ export class Main extends Component<any, MainState> {
|
||||
#
|
||||
</a>
|
||||
<a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
|
||||
<br></br>
|
||||
<br class="big"></br>
|
||||
<code>#</code>
|
||||
<br></br>
|
||||
<b>#</b>
|
||||
<br></br>
|
||||
<br class="big"></br>
|
||||
<a href={repoUrl}>#</a>
|
||||
<br></br>
|
||||
<br class="big"></br>
|
||||
<a href="https://www.rust-lang.org">#</a>
|
||||
<a href="https://actix.rs/">#</a>
|
||||
<a href="https://infernojs.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>
|
||||
</p>
|
||||
</div>
|
||||
@ -493,7 +497,7 @@ export class Main extends Component<any, MainState> {
|
||||
{i18n.t('prev')}
|
||||
</button>
|
||||
)}
|
||||
{this.state.posts.length == fetchLimit && (
|
||||
{this.state.posts.length > 0 && (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
|
85
ui/src/components/post-form.tsx
vendored
85
ui/src/components/post-form.tsx
vendored
@ -33,14 +33,14 @@ import {
|
||||
randomStr,
|
||||
setupTribute,
|
||||
setupTippy,
|
||||
emojiPicker,
|
||||
hostname,
|
||||
pictrsDeleteToast,
|
||||
validTitle,
|
||||
} from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import emojiShortName from 'emoji-short-name';
|
||||
import Selectr from 'mobius1-selectr';
|
||||
import Choices from 'choices.js';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
const MAX_POST_TITLE_LENGTH = 200;
|
||||
@ -70,6 +70,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
private id = `post-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private choices: Choices;
|
||||
private emptyState: PostFormState = {
|
||||
postForm: {
|
||||
name: null,
|
||||
@ -95,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.setupEmojiPicker();
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
@ -166,6 +166,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
this.choices && this.choices.destroy();
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
@ -271,12 +272,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
value={this.state.postForm.name}
|
||||
id="post-title"
|
||||
onInput={linkEvent(this, this.handlePostNameChange)}
|
||||
class="form-control"
|
||||
class={`form-control ${
|
||||
!validTitle(this.state.postForm.name) && 'is-invalid'
|
||||
}`}
|
||||
required
|
||||
rows={2}
|
||||
minLength={3}
|
||||
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 && (
|
||||
<>
|
||||
<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>
|
||||
</svg>
|
||||
</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>
|
||||
{!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) {
|
||||
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) {
|
||||
let res = wsJsonToRes(msg);
|
||||
if (msg.error) {
|
||||
@ -625,11 +606,45 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
// Set up select searching
|
||||
let selectId: any = document.getElementById('post-community');
|
||||
if (selectId) {
|
||||
let selector = new Selectr(selectId, { nativeDropdown: false });
|
||||
selector.on('selectr.select', option => {
|
||||
this.state.postForm.community_id = Number(option.value);
|
||||
this.setState(this.state);
|
||||
this.choices = new Choices(selectId, {
|
||||
shouldSort: false,
|
||||
classNames: {
|
||||
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) {
|
||||
let data = res.data as PostResponse;
|
||||
|
11
ui/src/components/post-listing.tsx
vendored
11
ui/src/components/post-listing.tsx
vendored
@ -33,6 +33,7 @@ import {
|
||||
setupTippy,
|
||||
hostname,
|
||||
previewLines,
|
||||
toast,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
@ -434,8 +435,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||
id: post.creator_id,
|
||||
local: post.creator_local,
|
||||
actor_id: post.creator_actor_id,
|
||||
published: post.creator_published,
|
||||
}}
|
||||
/>
|
||||
|
||||
{this.isMod && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
{i18n.t('mod')}
|
||||
@ -1030,6 +1033,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||
}
|
||||
|
||||
handlePostLike(i: PostListing) {
|
||||
if (!UserService.Instance.user) {
|
||||
this.context.router.history.push(`/login`);
|
||||
}
|
||||
|
||||
let new_vote = i.state.my_vote == 1 ? 0 : 1;
|
||||
|
||||
if (i.state.my_vote == 1) {
|
||||
@ -1057,6 +1064,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||
}
|
||||
|
||||
handlePostDisLike(i: PostListing) {
|
||||
if (!UserService.Instance.user) {
|
||||
this.context.router.history.push(`/login`);
|
||||
}
|
||||
|
||||
let new_vote = i.state.my_vote == -1 ? 0 : -1;
|
||||
|
||||
if (i.state.my_vote == 1) {
|
||||
|
173
ui/src/components/post.tsx
vendored
173
ui/src/components/post.tsx
vendored
@ -11,6 +11,7 @@ import {
|
||||
CommentForm as CommentFormI,
|
||||
CommentResponse,
|
||||
CommentSortType,
|
||||
CommentViewType,
|
||||
CommunityUser,
|
||||
CommunityResponse,
|
||||
CommentNode as CommentNodeI,
|
||||
@ -49,6 +50,7 @@ interface PostState {
|
||||
post: PostI;
|
||||
comments: Array<Comment>;
|
||||
commentSort: CommentSortType;
|
||||
commentViewType: CommentViewType;
|
||||
community: Community;
|
||||
moderators: Array<CommunityUser>;
|
||||
online: number;
|
||||
@ -65,6 +67,7 @@ export class Post extends Component<any, PostState> {
|
||||
post: null,
|
||||
comments: [],
|
||||
commentSort: CommentSortType.Hot,
|
||||
commentViewType: CommentViewType.Tree,
|
||||
community: null,
|
||||
moderators: [],
|
||||
online: null,
|
||||
@ -208,12 +211,12 @@ export class Post extends Component<any, PostState> {
|
||||
disabled={this.state.post.locked}
|
||||
/>
|
||||
{this.state.comments.length > 0 && this.sortRadios()}
|
||||
{this.commentsTree()}
|
||||
</div>
|
||||
<div class="col-12 col-sm-12 col-md-4">
|
||||
{this.state.comments.length > 0 && this.newComments()}
|
||||
{this.sidebar()}
|
||||
{this.state.commentViewType == CommentViewType.Tree &&
|
||||
this.commentsTree()}
|
||||
{this.state.commentViewType == CommentViewType.Chat &&
|
||||
this.commentsFlat()}
|
||||
</div>
|
||||
<div class="col-12 col-sm-12 col-md-4">{this.sidebar()}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -222,79 +225,94 @@ export class Post extends Component<any, PostState> {
|
||||
|
||||
sortRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle mb-2">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Hot && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('hot')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Hot}
|
||||
checked={this.state.commentSort === CommentSortType.Hot}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Top && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('top')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Top}
|
||||
checked={this.state.commentSort === CommentSortType.Top}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.New && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('new')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.New}
|
||||
checked={this.state.commentSort === CommentSortType.New}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Old && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('old')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Old}
|
||||
checked={this.state.commentSort === CommentSortType.Old}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<>
|
||||
<div class="btn-group btn-group-toggle mr-3 mb-2">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Hot && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('hot')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Hot}
|
||||
checked={this.state.commentSort === CommentSortType.Hot}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Top && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('top')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Top}
|
||||
checked={this.state.commentSort === CommentSortType.Top}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.New && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('new')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.New}
|
||||
checked={this.state.commentSort === CommentSortType.New}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Old && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('old')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Old}
|
||||
checked={this.state.commentSort === CommentSortType.Old}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</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 (
|
||||
<div class="d-none d-md-block new-comments mb-3 card border-secondary">
|
||||
<div class="card-body small">
|
||||
<h6>{i18n.t('recent_comments')}</h6>
|
||||
<CommentNodes
|
||||
nodes={commentsToFlatNodes(this.state.comments)}
|
||||
noIndent
|
||||
locked={this.state.post.locked}
|
||||
moderators={this.state.moderators}
|
||||
admins={this.state.siteRes.admins}
|
||||
postCreatorId={this.state.post.creator_id}
|
||||
showContext
|
||||
enableDownvotes={this.state.siteRes.site.enable_downvotes}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CommentNodes
|
||||
nodes={commentsToFlatNodes(this.state.comments)}
|
||||
noIndent
|
||||
locked={this.state.post.locked}
|
||||
moderators={this.state.moderators}
|
||||
admins={this.state.siteRes.admins}
|
||||
postCreatorId={this.state.post.creator_id}
|
||||
showContext
|
||||
enableDownvotes={this.state.siteRes.site.enable_downvotes}
|
||||
sort={this.state.commentSort}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -315,6 +333,13 @@ export class Post extends Component<any, PostState> {
|
||||
|
||||
handleCommentSortChange(i: Post, event: any) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
52
ui/src/components/search.tsx
vendored
52
ui/src/components/search.tsx
vendored
@ -148,7 +148,7 @@ export class Search extends Component<any, SearchState> {
|
||||
{this.state.type_ == SearchType.Posts && this.posts()}
|
||||
{this.state.type_ == SearchType.Communities && this.communities()}
|
||||
{this.state.type_ == SearchType.Users && this.users()}
|
||||
{this.noResults()}
|
||||
{this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
);
|
||||
@ -275,6 +275,7 @@ export class Search extends Component<any, SearchState> {
|
||||
{i.type_ == 'users' && (
|
||||
<div>
|
||||
<span>
|
||||
@
|
||||
<UserListing
|
||||
user={{
|
||||
name: (i.data as UserView).name,
|
||||
@ -282,9 +283,9 @@ export class Search extends Component<any, SearchState> {
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span>{` - ${
|
||||
(i.data as UserView).comment_score
|
||||
} comment karma`}</span>
|
||||
<span>{` - ${i18n.t('number_of_comments', {
|
||||
count: (i.data as UserView).number_of_comments,
|
||||
})}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -359,12 +360,17 @@ export class Search extends Component<any, SearchState> {
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<span>
|
||||
<Link
|
||||
className="text-info"
|
||||
to={`/u/${user.name}`}
|
||||
>{`/u/${user.name}`}</Link>
|
||||
@
|
||||
<UserListing
|
||||
user={{
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span>{` - ${user.comment_score} comment karma`}</span>
|
||||
<span>{` - ${i18n.t('number_of_comments', {
|
||||
count: user.number_of_comments,
|
||||
})}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -383,26 +389,26 @@ export class Search extends Component<any, SearchState> {
|
||||
{i18n.t('prev')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
{i18n.t('next')}
|
||||
</button>
|
||||
|
||||
{this.resultsCount() > 0 && (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
{i18n.t('next')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
noResults() {
|
||||
resultsCount(): number {
|
||||
let res = this.state.searchResponse;
|
||||
return (
|
||||
<div>
|
||||
{res &&
|
||||
res.posts.length == 0 &&
|
||||
res.comments.length == 0 &&
|
||||
res.communities.length == 0 &&
|
||||
res.users.length == 0 && <span>{i18n.t('no_results')}</span>}
|
||||
</div>
|
||||
res.posts.length +
|
||||
res.comments.length +
|
||||
res.communities.length +
|
||||
res.users.length
|
||||
);
|
||||
}
|
||||
|
||||
|
3
ui/src/components/symbols.tsx
vendored
3
ui/src/components/symbols.tsx
vendored
File diff suppressed because one or more lines are too long
35
ui/src/components/user-listing.tsx
vendored
35
ui/src/components/user-listing.tsx
vendored
@ -1,7 +1,13 @@
|
||||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { UserView } from '../interfaces';
|
||||
import { pictrsAvatarThumbnail, showAvatars, hostname } from '../utils';
|
||||
import {
|
||||
pictrsAvatarThumbnail,
|
||||
showAvatars,
|
||||
hostname,
|
||||
isCakeDay,
|
||||
} from '../utils';
|
||||
import { CakeDay } from './cake-day';
|
||||
|
||||
interface UserOther {
|
||||
name: string;
|
||||
@ -9,6 +15,7 @@ interface UserOther {
|
||||
avatar?: string;
|
||||
local?: boolean;
|
||||
actor_id?: string;
|
||||
published?: string;
|
||||
}
|
||||
|
||||
interface UserListingProps {
|
||||
@ -35,17 +42,21 @@ export class UserListing extends Component<UserListingProps, any> {
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="text-body font-weight-bold" to={link}>
|
||||
{user.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictrsAvatarThumbnail(user.avatar)}
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>{name_}</span>
|
||||
</Link>
|
||||
<>
|
||||
<Link className="text-body font-weight-bold" to={link}>
|
||||
{user.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictrsAvatarThumbnail(user.avatar)}
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>{name_}</span>
|
||||
</Link>
|
||||
|
||||
{isCakeDay(user.published) && <CakeDay creatorName={name_} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
57
ui/src/components/user.tsx
vendored
57
ui/src/components/user.tsx
vendored
@ -48,6 +48,7 @@ import { ListingTypeSelect } from './listing-type-select';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { i18n } from '../i18next';
|
||||
import moment from 'moment';
|
||||
|
||||
enum View {
|
||||
Overview,
|
||||
@ -382,6 +383,7 @@ export class User extends Component<any, UserState> {
|
||||
nodes={[{ comment: i.data as Comment }]}
|
||||
admins={this.state.admins}
|
||||
noIndent
|
||||
showCommunity
|
||||
showContext
|
||||
enableDownvotes={this.state.site.enable_downvotes}
|
||||
/>
|
||||
@ -399,6 +401,7 @@ export class User extends Component<any, UserState> {
|
||||
nodes={commentsToFlatNodes(this.state.comments)}
|
||||
admins={this.state.admins}
|
||||
noIndent
|
||||
showCommunity
|
||||
showContext
|
||||
enableDownvotes={this.state.site.enable_downvotes}
|
||||
/>
|
||||
@ -440,6 +443,15 @@ export class User extends Component<any, UserState> {
|
||||
)}
|
||||
</ul>
|
||||
</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>
|
||||
{i18n.t('joined')} <MomentTime data={user} showAgo />
|
||||
</div>
|
||||
@ -525,7 +537,7 @@ export class User extends Component<any, UserState> {
|
||||
htmlFor="file-upload"
|
||||
class="pointer ml-4 text-muted small font-weight-bold"
|
||||
>
|
||||
{!this.state.userSettingsForm.avatar ? (
|
||||
{!this.checkSettingsAvatar ? (
|
||||
<span class="btn btn-sm btn-secondary">
|
||||
{i18n.t('upload_avatar')}
|
||||
</span>
|
||||
@ -549,6 +561,18 @@ export class User extends Component<any, UserState> {
|
||||
/>
|
||||
</form>
|
||||
</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">
|
||||
<label>{i18n.t('language')}</label>
|
||||
<select
|
||||
@ -883,12 +907,14 @@ export class User extends Component<any, UserState> {
|
||||
{i18n.t('prev')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
{i18n.t('next')}
|
||||
</button>
|
||||
{this.state.comments.length + this.state.posts.length > 0 && (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
{i18n.t('next')}
|
||||
</button>
|
||||
)}
|
||||
</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) {
|
||||
event.preventDefault();
|
||||
i.state.userSettingsLoading = true;
|
||||
@ -1178,7 +1220,6 @@ export class User extends Component<any, UserState> {
|
||||
this.setState(this.state);
|
||||
} else if (res.op == UserOperation.SaveUserSettings) {
|
||||
let data = res.data as LoginResponse;
|
||||
this.state = this.emptyState;
|
||||
this.state.userSettingsLoading = false;
|
||||
this.setState(this.state);
|
||||
UserService.Instance.login(data);
|
||||
|
2
ui/src/index.html
vendored
2
ui/src/index.html
vendored
@ -13,7 +13,7 @@
|
||||
<!-- Styles -->
|
||||
<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/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/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)" />
|
||||
|
8
ui/src/interfaces.ts
vendored
8
ui/src/interfaces.ts
vendored
@ -54,6 +54,11 @@ export enum CommentSortType {
|
||||
Old,
|
||||
}
|
||||
|
||||
export enum CommentViewType {
|
||||
Tree,
|
||||
Chat,
|
||||
}
|
||||
|
||||
export enum ListingType {
|
||||
All,
|
||||
Subscribed,
|
||||
@ -183,6 +188,7 @@ export interface Post {
|
||||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
creator_name: string;
|
||||
creator_published: string;
|
||||
creator_avatar?: string;
|
||||
community_actor_id: string;
|
||||
community_local: boolean;
|
||||
@ -210,6 +216,7 @@ export interface Comment {
|
||||
local: boolean;
|
||||
creator_id: number;
|
||||
post_id: number;
|
||||
post_name: string;
|
||||
parent_id?: number;
|
||||
content: string;
|
||||
removed: boolean;
|
||||
@ -227,6 +234,7 @@ export interface Comment {
|
||||
creator_local: boolean;
|
||||
creator_name: string;
|
||||
creator_avatar?: string;
|
||||
creator_published: string;
|
||||
score: number;
|
||||
upvotes: number;
|
||||
downvotes: number;
|
||||
|
40
ui/src/utils.ts
vendored
40
ui/src/utils.ts
vendored
@ -51,11 +51,10 @@ import Tribute from 'tributejs/src/Tribute.js';
|
||||
import markdown_it from 'markdown-it';
|
||||
import markdownitEmoji from 'markdown-it-emoji/light';
|
||||
import markdown_it_container from 'markdown-it-container';
|
||||
import twemoji from 'twemoji';
|
||||
import emojiShortName from 'emoji-short-name';
|
||||
import Toastify from 'toastify-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 helpGuideUrl = '/docs/about_guide.html';
|
||||
@ -114,14 +113,6 @@ export const themes = [
|
||||
'litely',
|
||||
];
|
||||
|
||||
export const emojiPicker = new EmojiButton({
|
||||
// Use the emojiShortName from native
|
||||
style: 'twemoji',
|
||||
theme: 'dark',
|
||||
position: 'auto-start',
|
||||
// TODO i18n
|
||||
});
|
||||
|
||||
const DEFAULT_ALPHABET =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
@ -178,10 +169,6 @@ export const md = new markdown_it({
|
||||
defs: objectFlip(emojiShortName),
|
||||
});
|
||||
|
||||
md.renderer.rules.emoji = function (token, idx) {
|
||||
return twemoji.parse(token[idx].content);
|
||||
};
|
||||
|
||||
export function hotRankComment(comment: Comment): number {
|
||||
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
|
||||
export function pictrsImage(hash: string, thumbnail: boolean = false): string {
|
||||
let root = `/pictrs/image`;
|
||||
@ -590,8 +590,7 @@ export function setupTribute(): Tribute {
|
||||
trigger: ':',
|
||||
menuItemTemplate: (item: any) => {
|
||||
let shortName = `:${item.original.key}:`;
|
||||
let twemojiIcon = twemoji.parse(item.original.val);
|
||||
return `${twemojiIcon} ${shortName}`;
|
||||
return `${item.original.val} ${shortName}`;
|
||||
},
|
||||
selectTemplate: (item: any) => {
|
||||
return `:${item.original.key}:`;
|
||||
@ -988,3 +987,12 @@ function canUseWebP() {
|
||||
// // very old browser like IE 8, canvas not supported
|
||||
// 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
2
ui/src/version.ts
vendored
@ -1 +1 @@
|
||||
export const version: string = 'v0.7.13';
|
||||
export const version: string = 'v0.7.19';
|
||||
|
8
ui/translations/en.json
vendored
8
ui/translations/en.json
vendored
@ -217,9 +217,10 @@
|
||||
"no": "no",
|
||||
"powered_by": "Powered by",
|
||||
"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.",
|
||||
"logged_in": "Logged in.",
|
||||
"must_login": "You must <1>log in or register</1> to comment.",
|
||||
"site_saved": "Site Saved.",
|
||||
"community_ban": "You have been banned from this community.",
|
||||
"site_ban": "You have been banned from the site",
|
||||
@ -265,5 +266,8 @@
|
||||
"action": "Action",
|
||||
"emoji_picker": "Emoji Picker",
|
||||
"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…
Reference in New Issue
Block a user