- 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:
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/
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
restart: always
lemmy:
- image: dessalines/lemmy:v0.7.13
+ image: dessalines/lemmy:v0.7.21
ports:
- "127.0.0.1:8536:8536"
restart: always
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`.
```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
page: Option<i64>,
limit: Option<i64>,
community_id: Option<i32>,
+ community_name: Option<String>,
auth: Option<String>
}
}
-#!/bin/sh
+#!/bin/bash
set -e
# Set the database variable to the default first.
"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"
[[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",
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"
"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",
"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"
[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"
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"
-#!/bin/sh
+#!/bin/bash
+set -e
# Default configurations
username=lemmy
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
-file = "src/schema.rs"
+file = "lemmy_db/src/schema.rs"
--- /dev/null
+[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"
\ No newline at end of file
-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"]
}
}
-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() {
use crate::{
- db::Crud,
schema::{category, category::dsl::*},
+ Crud,
};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[cfg(test)]
mod tests {
- use super::*;
- use crate::db::establish_unpooled_connection;
+ use crate::{category::Category, tests::establish_unpooled_connection};
#[test]
fn test_crud() {
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
}
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)
}
#[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();
// 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};
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
+ post_name -> Varchar,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
score -> BigInt,
upvotes -> BigInt,
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
+ post_name -> Varchar,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
score -> BigInt,
upvotes -> BigInt,
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,
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,
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
+ post_name -> Varchar,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
+ creator_published -> Timestamp,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
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,
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,
#[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() {
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,
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,
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,
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,
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};
#[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() {
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};
use super::community_view::community_moderator_view::dsl::*;
community_moderator_view
.filter(community_id.eq(from_community_id))
+ .order_by(published)
.load::<Self>(conn)
}
use super::community_view::community_moderator_view::dsl::*;
community_moderator_view
.filter(user_id.eq(from_user_id))
+ .order_by(published)
.load::<Self>(conn)
}
}
-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;
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;
}
}
-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)]
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";
use crate::{
- db::Crud,
schema::{
mod_add,
mod_add_community,
mod_remove_post,
mod_sticky_post,
},
+ Crud,
};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[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]
-use crate::db::limit_and_offset;
+use crate::limit_and_offset;
use diesel::{result::Error, *};
use serde::{Deserialize, Serialize};
use crate::{
- db::Crud,
schema::{password_reset_request, password_reset_request::dsl::*},
+ Crud,
};
use diesel::{dsl::*, result::Error, *};
use sha2::{Digest, Sha256};
#[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() {
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};
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)
}
#[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() {
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};
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
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,
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,
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,
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
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));
}
#[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() {
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,
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,
-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};
}
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)
}
#[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() {
-use crate::db::{limit_and_offset, MaybeOptional};
+use crate::{limit_and_offset, MaybeOptional};
use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize};
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>,
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>,
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>,
-use crate::{db::Crud, schema::site};
+use crate::{schema::site, Crud};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
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_"]
}
}
-#[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() {
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};
#[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() {
-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};
creator_actor_id -> Text,
creator_local -> Bool,
post_id -> Int4,
+ post_name -> Varchar,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
post_id -> Int4,
+ post_name -> Varchar,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
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,
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};
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> {
--- /dev/null
+[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"] }
\ No newline at end of file
--- /dev/null
+#[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()
+}
-use crate::LemmyError;
use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;
-use std::{env, fs, net::IpAddr, sync::RwLock};
+use std::{env, fs, io::Error, net::IpAddr, sync::RwLock};
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
static CONFIG_FILE: &str = "config/config.hjson";
/// 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
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
--- /dev/null
+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;
\ No newline at end of file
--- /dev/null
+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;
\ No newline at end of file
--- /dev/null
+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
+;
\ No newline at end of file
--- /dev/null
+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
+;
\ No newline at end of file
--- /dev/null
+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)
+ }
+}
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,
},
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;
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?
{
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
+ let mut editors: Vec<i32> = vec![orig_comment.creator_id];
+ let mut moderators: Vec<i32> = vec![];
+
+ let community_id = orig_comment.community_id;
+ moderators.append(
+ &mut blocking(pool, move |conn| {
+ CommunityModeratorView::for_community(&conn, community_id)
+ .map(|v| v.into_iter().map(|m| m.user_id).collect())
+ })
+ .await??,
+ );
+ moderators.append(
+ &mut blocking(pool, move |conn| {
+ UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
+ })
+ .await??,
+ );
+
+ editors.extend(&moderators);
// You are allowed to mark the comment as read even if you're banned.
if data.read.is_none() {
// Verify its the creator or a mod, or an admin
- let mut editors: Vec<i32> = vec![data.creator_id];
- let community_id = orig_comment.community_id;
- editors.append(
- &mut blocking(pool, move |conn| {
- Ok(
- CommunityModeratorView::for_community(&conn, community_id)?
- .into_iter()
- .map(|m| m.user_id)
- .collect(),
- ) as Result<_, LemmyError>
- })
- .await??,
- );
- editors.append(
- &mut blocking(pool, move |conn| {
- Ok(UserView::admins(conn)?.into_iter().map(|a| a.id).collect()) as Result<_, LemmyError>
- })
- .await??,
- );
if !editors.contains(&user_id) {
return Err(APIError::err("no_comment_edit_allowed").into());
if user.banned {
return Err(APIError::err("site_ban").into());
}
+ } else {
+ // check that user can mark as read
+ let parent_id = orig_comment.parent_id;
+ match parent_id {
+ Some(pid) => {
+ let parent_comment =
+ blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
+ if user_id != parent_comment.creator_id {
+ return Err(APIError::err("no_comment_edit_allowed").into());
+ }
+ }
+ None => {
+ let parent_post_id = orig_comment.post_id;
+ let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
+ if user_id != parent_post.creator_id {
+ return Err(APIError::err("no_comment_edit_allowed").into());
+ }
+ }
+ }
}
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let edit_id = data.edit_id;
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
- let comment_form = CommentForm {
- content: content_slurs_removed,
- parent_id: data.parent_id,
- post_id: data.post_id,
- creator_id: data.creator_id,
- removed: data.removed.to_owned(),
- deleted: data.deleted.to_owned(),
- read: data.read.to_owned(),
- published: None,
- updated: if data.read.is_some() {
- orig_comment.updated
+ let comment_form = {
+ if data.read.is_none() {
+ // the ban etc checks should been made and have passed
+ // the comment can be properly edited
+ let post_removed = if moderators.contains(&user_id) {
+ data.removed
+ } else {
+ Some(read_comment.removed)
+ };
+
+ CommentForm {
+ content: content_slurs_removed,
+ parent_id: read_comment.parent_id,
+ post_id: read_comment.post_id,
+ creator_id: read_comment.creator_id,
+ removed: post_removed.to_owned(),
+ deleted: data.deleted.to_owned(),
+ read: Some(read_comment.read),
+ published: None,
+ updated: Some(naive_now()),
+ ap_id: read_comment.ap_id,
+ local: read_comment.local,
+ }
} else {
- Some(naive_now())
- },
- ap_id: read_comment.ap_id,
- local: read_comment.local,
+ // the only field that can be updated it the read field
+ CommentForm {
+ content: read_comment.content,
+ parent_id: read_comment.parent_id,
+ post_id: read_comment.post_id,
+ creator_id: read_comment.creator_id,
+ removed: Some(read_comment.removed).to_owned(),
+ deleted: Some(read_comment.deleted).to_owned(),
+ read: data.read.to_owned(),
+ published: None,
+ updated: orig_comment.updated,
+ ap_id: read_comment.ap_id,
+ local: read_comment.local,
+ }
+ }
};
let edit_id = data.edit_id;
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
- if let Some(deleted) = data.deleted.to_owned() {
- if deleted {
- updated_comment
- .send_delete(&user, &self.client, pool)
- .await?;
+ if data.read.is_none() {
+ if let Some(deleted) = data.deleted.to_owned() {
+ if deleted {
+ updated_comment
+ .send_delete(&user, &self.client, pool)
+ .await?;
+ } else {
+ updated_comment
+ .send_undo_delete(&user, &self.client, pool)
+ .await?;
+ }
+ } else if let Some(removed) = data.removed.to_owned() {
+ if moderators.contains(&user_id) {
+ if removed {
+ updated_comment
+ .send_remove(&user, &self.client, pool)
+ .await?;
+ } else {
+ updated_comment
+ .send_undo_remove(&user, &self.client, pool)
+ .await?;
+ }
+ }
} else {
updated_comment
- .send_undo_delete(&user, &self.client, pool)
+ .send_update(&user, &self.client, pool)
.await?;
}
- } else if let Some(removed) = data.removed.to_owned() {
- if removed {
- updated_comment
- .send_remove(&user, &self.client, pool)
- .await?;
- } else {
- updated_comment
- .send_undo_remove(&user, &self.client, pool)
- .await?;
+
+ // Mod tables
+ if moderators.contains(&user_id) {
+ if let Some(removed) = data.removed.to_owned() {
+ let form = ModRemoveCommentForm {
+ mod_user_id: user_id,
+ comment_id: data.edit_id,
+ removed: Some(removed),
+ reason: data.reason.to_owned(),
+ };
+ blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
+ }
}
- } else {
- updated_comment
- .send_update(&user, &self.client, pool)
- .await?;
}
let post_id = data.post_id;
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
- // Mod tables
- if let Some(removed) = data.removed.to_owned() {
- let form = ModRemoveCommentForm {
- mod_user_id: user_id,
- comment_id: data.edit_id,
- removed: Some(removed),
- reason: data.reason.to_owned(),
- };
- blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
- }
-
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
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;
title: data.title.to_owned(),
description: data.description.to_owned(),
category_id: data.category_id.to_owned(),
- creator_id: user_id,
+ creator_id: read_community.creator_id,
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
let user_id = claims.id;
+ let mut community_moderators: Vec<i32> = vec![];
+
+ let community_id = data.community_id;
+
+ community_moderators.append(
+ &mut blocking(pool, move |conn| {
+ CommunityModeratorView::for_community(&conn, community_id)
+ .map(|v| v.into_iter().map(|m| m.user_id).collect())
+ })
+ .await??,
+ );
+ community_moderators.append(
+ &mut blocking(pool, move |conn| {
+ UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
+ })
+ .await??,
+ );
+
+ if !community_moderators.contains(&user_id) {
+ return Err(APIError::err("couldnt_update_community").into());
+ }
+
let community_user_ban_form = CommunityUserBanForm {
community_id: data.community_id,
user_id: data.user_id,
user_id: data.user_id,
};
+ let mut community_moderators: Vec<i32> = vec![];
+
+ let community_id = data.community_id;
+
+ community_moderators.append(
+ &mut blocking(pool, move |conn| {
+ CommunityModeratorView::for_community(&conn, community_id)
+ .map(|v| v.into_iter().map(|m| m.user_id).collect())
+ })
+ .await??,
+ );
+ community_moderators.append(
+ &mut blocking(pool, move |conn| {
+ UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
+ })
+ .await??,
+ );
+
+ if !community_moderators.contains(&user_id) {
+ return Err(APIError::err("couldnt_update_community").into());
+ }
+
if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(pool, join).await?.is_err() {
-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;
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,
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;
page: Option<i64>,
limit: Option<i64>,
pub community_id: Option<i32>,
+ pub community_name: Option<String>,
auth: Option<String>,
}
}
}
+ 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
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,
};
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?;
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)
}
}
+ 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()),
let user_id = claims.id;
+ let edit_id = data.edit_id;
+ let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
+
// Verify its the creator or a mod or admin
- let community_id = data.community_id;
- let mut editors: Vec<i32> = vec![data.creator_id];
- editors.append(
+ let community_id = read_post.community_id;
+ let mut editors: Vec<i32> = vec![read_post.creator_id];
+ let mut moderators: Vec<i32> = vec![];
+
+ moderators.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
- editors.append(
+ moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
+
+ editors.extend(&moderators);
+
if !editors.contains(&user_id) {
return Err(APIError::err("no_post_edit_allowed").into());
}
// Check for a community ban
- let community_id = data.community_id;
+ let community_id = read_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
- let edit_id = data.edit_id;
- let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
-
- let post_form = PostForm {
- name: data.name.to_owned(),
- url: data.url.to_owned(),
- body: data.body.to_owned(),
- creator_id: data.creator_id.to_owned(),
- community_id: data.community_id,
- removed: data.removed.to_owned(),
- deleted: data.deleted.to_owned(),
- nsfw: data.nsfw,
- locked: data.locked.to_owned(),
- stickied: data.stickied.to_owned(),
- updated: Some(naive_now()),
- embed_title: iframely_title,
- embed_description: iframely_description,
- embed_html: iframely_html,
- thumbnail_url: pictrs_thumbnail,
- ap_id: read_post.ap_id,
- local: read_post.local,
- published: None,
+ let post_form = {
+ // only modify some properties if they are a moderator
+ if moderators.contains(&user_id) {
+ PostForm {
+ name: data.name.trim().to_owned(),
+ url: data.url.to_owned(),
+ body: data.body.to_owned(),
+ creator_id: read_post.creator_id.to_owned(),
+ community_id: read_post.community_id,
+ removed: data.removed.to_owned(),
+ deleted: data.deleted.to_owned(),
+ nsfw: data.nsfw,
+ locked: data.locked.to_owned(),
+ stickied: data.stickied.to_owned(),
+ updated: Some(naive_now()),
+ embed_title: iframely_title,
+ embed_description: iframely_description,
+ embed_html: iframely_html,
+ thumbnail_url: pictrs_thumbnail,
+ ap_id: read_post.ap_id,
+ local: read_post.local,
+ published: None,
+ }
+ } else {
+ PostForm {
+ name: read_post.name.trim().to_owned(),
+ url: data.url.to_owned(),
+ body: data.body.to_owned(),
+ creator_id: read_post.creator_id.to_owned(),
+ community_id: read_post.community_id,
+ removed: Some(read_post.removed),
+ deleted: data.deleted.to_owned(),
+ nsfw: data.nsfw,
+ locked: Some(read_post.locked),
+ stickied: Some(read_post.stickied),
+ updated: Some(naive_now()),
+ embed_title: iframely_title,
+ embed_description: iframely_description,
+ embed_html: iframely_html,
+ thumbnail_url: pictrs_thumbnail,
+ ap_id: read_post.ap_id,
+ local: read_post.local,
+ published: None,
+ }
+ }
};
let edit_id = data.edit_id;
}
};
- // Mod tables
- if let Some(removed) = data.removed.to_owned() {
- let form = ModRemovePostForm {
- mod_user_id: user_id,
- post_id: data.edit_id,
- removed: Some(removed),
- reason: data.reason.to_owned(),
- };
- blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
- }
+ if moderators.contains(&user_id) {
+ // Mod tables
+ if let Some(removed) = data.removed.to_owned() {
+ let form = ModRemovePostForm {
+ mod_user_id: user_id,
+ post_id: data.edit_id,
+ removed: Some(removed),
+ reason: data.reason.to_owned(),
+ };
+ blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
+ }
- if let Some(locked) = data.locked.to_owned() {
- let form = ModLockPostForm {
- mod_user_id: user_id,
- post_id: data.edit_id,
- locked: Some(locked),
- };
- blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
- }
+ if let Some(locked) = data.locked.to_owned() {
+ let form = ModLockPostForm {
+ mod_user_id: user_id,
+ post_id: data.edit_id,
+ locked: Some(locked),
+ };
+ blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
+ }
- if let Some(stickied) = data.stickied.to_owned() {
- let form = ModStickyPostForm {
- mod_user_id: user_id,
- post_id: data.edit_id,
- stickied: Some(stickied),
- };
- blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
+ if let Some(stickied) = data.stickied.to_owned() {
+ let form = ModStickyPostForm {
+ mod_user_id: user_id,
+ post_id: data.edit_id,
+ stickied: Some(stickied),
+ };
+ blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
+ }
}
if let Some(deleted) = data.deleted.to_owned() {
.await?;
}
} else if let Some(removed) = data.removed.to_owned() {
- if removed {
- updated_post.send_remove(&user, &self.client, pool).await?;
- } else {
- updated_post
- .send_undo_remove(&user, &self.client, pool)
- .await?;
+ if moderators.contains(&user_id) {
+ if removed {
+ updated_post.send_remove(&user, &self.client, pool).await?;
+ } else {
+ updated_post
+ .send_undo_remove(&user, &self.client, pool)
+ .await?;
+ }
}
} else {
updated_post.send_update(&user, &self.client, pool).await?;
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;
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,
+ websocket::{
+ server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
+ UserOperation,
+ WebsocketInfo,
},
+ DbPool,
+ LemmyError,
+};
+use bcrypt::verify;
+use lemmy_db::{
+ comment::*,
+ comment_view::*,
+ community::*,
+ community_view::*,
+ moderator::*,
+ naive_now,
+ password_reset_request::*,
+ post::*,
+ post_view::*,
+ private_message::*,
+ private_message_view::*,
+ site::*,
+ site_view::*,
+ user::*,
+ user_mention::*,
+ user_mention_view::*,
+ user_view::*,
+ Crud,
+ Followable,
+ Joinable,
+ ListingType,
+ SortType,
+};
+use lemmy_utils::{
+ generate_actor_keypair,
generate_random_string,
is_valid_username,
+ make_apub_endpoint,
naive_from_unix,
- naive_now,
remove_slurs,
send_email,
settings::Settings,
slur_check,
slurs_vec_to_str,
- websocket::{
- server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
- UserOperation,
- WebsocketInfo,
- },
- DbPool,
- LemmyError,
+ EndpointType,
};
-use bcrypt::verify;
use log::error;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
// 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?
{
}
// Return the jwt
- Ok(LoginResponse { jwt: user.jwt() })
+ Ok(LoginResponse {
+ jwt: Claims::jwt(user, Settings::get().hostname),
+ })
}
}
// Return the jwt
Ok(LoginResponse {
- jwt: inserted_user.jwt(),
+ jwt: Claims::jwt(inserted_user, Settings::get().hostname),
})
}
}
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 {
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()),
// Return the jwt
Ok(LoginResponse {
- jwt: updated_user.jwt(),
+ jwt: Claims::jwt(updated_user, Settings::get().hostname),
})
}
}
let user_id = claims.id;
let user_mention_id = data.user_mention_id;
- let user_mention =
+ let read_user_mention =
blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??;
+ if user_id != read_user_mention.recipient_id {
+ return Err(APIError::err("couldnt_update_comment").into());
+ }
+
let user_mention_form = UserMentionForm {
- recipient_id: user_id,
- comment_id: user_mention.comment_id,
+ recipient_id: read_user_mention.recipient_id,
+ comment_id: read_user_mention.comment_id,
read: data.read.to_owned(),
};
- let user_mention_id = user_mention.id;
+ let user_mention_id = read_user_mention.id;
let update_mention =
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form);
if blocking(pool, update_mention).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
};
- let user_mention_id = user_mention.id;
+ let user_mention_id = read_user_mention.id;
let user_mention_view = blocking(pool, move |conn| {
UserMentionView::read(conn, user_mention_id, user_id)
})
// Return the jwt
Ok(LoginResponse {
- jwt: updated_user.jwt(),
+ jwt: Claims::jwt(updated_user, Settings::get().hostname),
})
}
}
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?
{
let content_slurs_removed = match &data.content {
Some(content) => remove_slurs(content),
- None => orig_private_message.content,
+ None => orig_private_message.content.clone(),
};
- let private_message_form = PrivateMessageForm {
- content: content_slurs_removed,
- creator_id: orig_private_message.creator_id,
- recipient_id: orig_private_message.recipient_id,
- deleted: data.deleted.to_owned(),
- read: data.read.to_owned(),
- updated: if data.read.is_some() {
- orig_private_message.updated
+ let private_message_form = {
+ if data.read.is_some() {
+ PrivateMessageForm {
+ content: orig_private_message.content.to_owned(),
+ creator_id: orig_private_message.creator_id,
+ recipient_id: orig_private_message.recipient_id,
+ read: data.read.to_owned(),
+ updated: orig_private_message.updated,
+ deleted: Some(orig_private_message.deleted),
+ ap_id: orig_private_message.ap_id,
+ local: orig_private_message.local,
+ published: None,
+ }
} else {
- Some(naive_now())
- },
- ap_id: orig_private_message.ap_id,
- local: orig_private_message.local,
- published: None,
+ PrivateMessageForm {
+ content: content_slurs_removed,
+ creator_id: orig_private_message.creator_id,
+ recipient_id: orig_private_message.recipient_id,
+ deleted: data.deleted.to_owned(),
+ read: Some(orig_private_message.read),
+ updated: Some(naive_now()),
+ ap_id: orig_private_message.ap_id,
+ local: orig_private_message.local,
+ published: None,
+ }
+ }
};
let edit_id = data.edit_id;
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
- if let Some(deleted) = data.deleted.to_owned() {
- if deleted {
- updated_private_message
- .send_delete(&user, &self.client, pool)
- .await?;
+ if data.read.is_none() {
+ if let Some(deleted) = data.deleted.to_owned() {
+ if deleted {
+ updated_private_message
+ .send_delete(&user, &self.client, pool)
+ .await?;
+ } else {
+ updated_private_message
+ .send_undo_delete(&user, &self.client, pool)
+ .await?;
+ }
} else {
updated_private_message
- .send_undo_delete(&user, &self.client, pool)
+ .send_update(&user, &self.client, pool)
.await?;
}
} else {
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;
// 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?;
}
ToApub,
},
blocking,
- convert_datetime,
- db::{
- comment::{Comment, CommentForm},
- community::Community,
- post::Post,
- user::User_,
- Crud,
- },
routes::DbPoolParam,
- scrape_text_for_mentions,
DbPool,
LemmyError,
- MentionData,
};
use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
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;
/// 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> {
extensions::group_extensions::GroupExtension,
fetcher::get_or_fetch_and_upsert_remote_user,
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,
};
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};
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()
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(),
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)?)?;
-
- 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?;
-
- // 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?;
-
- Ok(HttpResponse::Ok().finish())
- }
+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?;
+
+ // 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());
+
+ send_activity(client, &announce, community, to).await?;
+
+ Ok(HttpResponse::Ok().finish())
}
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;
-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)]
use openssl::{
hash::MessageDigest,
pkey::PKey,
- rsa::Rsa,
sign::{Signer, Verifier},
};
use serde::{Deserialize, Serialize};
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,
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,
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};
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()
.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??;
// 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??;
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)
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??;
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()
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??;
"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??;
page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension},
},
- convert_datetime,
- db::user::User_,
+ blocking,
request::{retry, RecvError},
routes::webfinger::WebFingerResponse,
DbPool,
LemmyError,
- MentionData,
- Settings,
};
use activitystreams::object::Page;
use activitystreams_ext::{Ext1, Ext2};
};
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;
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>
.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);
pub trait FromApub {
type ApubType;
async fn from_apub(
- apub: &mut Self::ApubType,
+ apub: &Self::ApubType,
client: &Client,
pool: &DbPool,
) -> Result<Self, LemmyError>
.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(())
+}
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,
ToApub,
},
blocking,
- convert_datetime,
- db::{
- community::Community,
- post::{Post, PostForm},
- user::User_,
- Crud,
- },
routes::DbPoolParam,
DbPool,
LemmyError,
- Settings,
};
use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
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)]
/// 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> {
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,
};
};
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 {
/// 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> {
post::PostResponse,
},
apub::{
+ community::do_announce,
extensions::signatures::verify,
fetcher::{
get_or_fetch_and_insert_remote_comment,
get_or_fetch_and_upsert_remote_community,
get_or_fetch_and_upsert_remote_user,
},
+ 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,
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;
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())
}
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut page = create
+ let page = create
.create_props
.get_object_base_box()
.to_owned()
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??;
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = create
+ let note = create
.create_props
.get_object_base_box()
.to_owned()
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??;
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut page = update
+ let page = update
.update_props
.get_object_base_box()
.to_owned()
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?
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut page = like
+ let page = like
.like_props
.get_object_base_box()
.to_owned()
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?
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut page = dislike
+ let page = dislike
.dislike_props
.get_object_base_box()
.to_owned()
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?
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = update
+ let note = update
.update_props
.get_object_base_box()
.to_owned()
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?
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = like
+ let note = like
.like_props
.get_object_base_box()
.to_owned()
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?
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = dislike
+ let note = dislike
.dislike_props
.get_object_base_box()
.to_owned()
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?
.unwrap()
.to_string();
- let mut group = delete
+ let group = delete
.delete_props
.get_object_base_box()
.to_owned()
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;
.unwrap()
.to_string();
- let mut group = remove
+ let group = remove
.remove_props
.get_object_base_box()
.to_owned()
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;
.unwrap()
.to_string();
- let mut page = delete
+ let page = delete
.delete_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut page = remove
+ let page = remove
.remove_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut note = delete
+ let note = delete
.delete_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut note = remove
+ let note = remove
.remove_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut note = delete
+ let note = delete
.delete_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut note = remove
+ let note = remove
.remove_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut page = delete
+ let page = delete
.delete_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut page = remove
+ let page = remove
.remove_props
.get_object_base_box()
.to_owned()
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?;
.unwrap()
.to_string();
- let mut group = delete
+ let group = delete
.delete_props
.get_object_base_box()
.to_owned()
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;
.unwrap()
.to_string();
- let mut group = remove
+ let group = remove
.remove_props
.get_object_base_box()
.to_owned()
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;
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = like
+ let note = like
.like_props
.get_object_base_box()
.to_owned()
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?
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut page = like
+ let page = like
.like_props
.get_object_base_box()
.to_owned()
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?
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,
};
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)]
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()
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(),
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,
) -> 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?;
apub::{
extensions::signatures::verify,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
+ 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,
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;
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = create
+ let note = create
.create_props
.get_object_base_box()
.to_owned()
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)
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = update
+ let note = update
.update_props
.get_object_base_box()
.to_owned()
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| {
pool: &DbPool,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
- let mut note = delete
+ let note = delete
.delete_props
.get_object_base_box()
.to_owned()
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| {
.to_owned()
.into_concrete::<Delete>()?;
- let mut note = delete
+ let note = delete
.delete_props
.get_object_base_box()
.to_owned()
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| {
// 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> {
}
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");
}
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");
}
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");
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());
}
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");
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)?;
}
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");
.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());
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>>;
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>,
}
}
-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())
- }
-}
-
-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)
+impl std::fmt::Display for LemmyError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ self.inner.fmt(f)
}
}
-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 {
}
}
-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 {
.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,
-}
-
-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 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?;
-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
// 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();
}
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);
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| {
-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,
-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("/")
.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)),
+ );
}
}
-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;
) -> 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)
-use crate::settings::Settings;
use actix_files::NamedFile;
use actix_web::*;
+use lemmy_utils::settings::Settings;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg
-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;
-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)]
}
}
-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
-pub const VERSION: &str = "v0.7.13";
+pub const VERSION: &str = "v0.7.21";
--- /dev/null
+#!/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
--- /dev/null
+.choices{position:relative;margin-bottom:24px;font-size:16px}.choices:focus{outline:0}.choices:last-child{margin-bottom:0}.choices.is-disabled .choices__inner,.choices.is-disabled .choices__input{background-color:#eaeaea;cursor:not-allowed;-webkit-user-select:none;-ms-user-select:none;user-select:none}.choices.is-disabled .choices__item{cursor:not-allowed}.choices [hidden]{display:none!important}.choices[data-type*=select-one]{cursor:pointer}.choices[data-type*=select-one] .choices__inner{padding-bottom:7.5px}.choices[data-type*=select-one] .choices__input{display:block;width:100%;padding:10px;border-bottom:1px solid #ddd;background-color:#fff;margin:0}.choices[data-type*=select-one] .choices__button{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);padding:0;background-size:8px;position:absolute;top:50%;right:0;margin-top:-10px;margin-right:25px;height:20px;width:20px;border-radius:10em;opacity:.5}.choices[data-type*=select-one] .choices__button:focus,.choices[data-type*=select-one] .choices__button:hover{opacity:1}.choices[data-type*=select-one] .choices__button:focus{box-shadow:0 0 0 2px #00bcd4}.choices[data-type*=select-one] .choices__item[data-value=''] .choices__button{display:none}.choices[data-type*=select-one]:after{content:'';height:0;width:0;border-style:solid;border-color:#333 transparent transparent;border-width:5px;position:absolute;right:11.5px;top:50%;margin-top:-2.5px;pointer-events:none}.choices[data-type*=select-one].is-open:after{border-color:transparent transparent #333;margin-top:-7.5px}.choices[data-type*=select-one][dir=rtl]:after{left:11.5px;right:auto}.choices[data-type*=select-one][dir=rtl] .choices__button{right:auto;left:0;margin-left:25px;margin-right:0}.choices[data-type*=select-multiple] .choices__inner,.choices[data-type*=text] .choices__inner{cursor:text}.choices[data-type*=select-multiple] .choices__button,.choices[data-type*=text] .choices__button{position:relative;display:inline-block;margin:0 -4px 0 8px;padding-left:16px;border-left:1px solid #008fa1;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);background-size:8px;width:8px;line-height:1;opacity:.75;border-radius:0}.choices[data-type*=select-multiple] .choices__button:focus,.choices[data-type*=select-multiple] .choices__button:hover,.choices[data-type*=text] .choices__button:focus,.choices[data-type*=text] .choices__button:hover{opacity:1}.choices__inner{display:inline-block;vertical-align:top;width:100%;background-color:#f9f9f9;padding:7.5px 7.5px 3.75px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;min-height:44px;overflow:hidden}.is-focused .choices__inner,.is-open .choices__inner{border-color:#b7b7b7}.is-open .choices__inner{border-radius:2.5px 2.5px 0 0}.is-flipped.is-open .choices__inner{border-radius:0 0 2.5px 2.5px}.choices__list{margin:0;padding-left:0;list-style:none}.choices__list--single{display:inline-block;padding:4px 16px 4px 4px;width:100%}[dir=rtl] .choices__list--single{padding-right:4px;padding-left:16px}.choices__list--single .choices__item{width:100%}.choices__list--multiple{display:inline}.choices__list--multiple .choices__item{display:inline-block;vertical-align:middle;border-radius:20px;padding:4px 10px;font-size:12px;font-weight:500;margin-right:3.75px;margin-bottom:3.75px;background-color:#00bcd4;border:1px solid #00a5bb;color:#fff;word-break:break-all;box-sizing:border-box}.choices__list--multiple .choices__item[data-deletable]{padding-right:5px}[dir=rtl] .choices__list--multiple .choices__item{margin-right:0;margin-left:3.75px}.choices__list--multiple .choices__item.is-highlighted{background-color:#00a5bb;border:1px solid #008fa1}.is-disabled .choices__list--multiple .choices__item{background-color:#aaa;border:1px solid #919191}.choices__list--dropdown{visibility:hidden;z-index:1;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;border-bottom-left-radius:2.5px;border-bottom-right-radius:2.5px;overflow:hidden;word-break:break-all;will-change:visibility}.choices__list--dropdown.is-active{visibility:visible}.is-open .choices__list--dropdown{border-color:#b7b7b7}.is-flipped .choices__list--dropdown{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-radius:.25rem .25rem 0 0}.choices__list--dropdown .choices__list{position:relative;max-height:300px;overflow:auto;-webkit-overflow-scrolling:touch;will-change:scroll-position}.choices__list--dropdown .choices__item{position:relative;padding:10px;font-size:14px}[dir=rtl] .choices__list--dropdown .choices__item{text-align:right}@media (min-width:640px){.choices__list--dropdown .choices__item--selectable{padding-right:100px}.choices__list--dropdown .choices__item--selectable:after{content:attr(data-select-text);font-size:12px;opacity:0;position:absolute;right:10px;top:50%;transform:translateY(-50%)}[dir=rtl] .choices__list--dropdown .choices__item--selectable{text-align:right;padding-left:100px;padding-right:10px}[dir=rtl] .choices__list--dropdown .choices__item--selectable:after{right:auto;left:10px}}.choices__list--dropdown .choices__item--selectable.is-highlighted{background-color:#f2f2f2}.choices__list--dropdown .choices__item--selectable.is-highlighted:after{opacity:.5}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;-ms-user-select:none;user-select:none;opacity:.5}.choices__heading{font-weight:600;font-size:12px;padding:10px;border-bottom:1px solid #f7f7f7;color:gray}.choices__button{text-indent:-9999px;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.choices__button:focus,.choices__input:focus{outline:0}.choices__input{display:inline-block;vertical-align:baseline;background-color:#f9f9f9;font-size:14px;margin-bottom:5px;border:0;border-radius:0;max-width:100%;padding:4px 0 4px 2px}[dir=rtl] .choices__input{padding-right:2px;padding-left:0}.choices__placeholder{opacity:.5}
\ No newline at end of file
width: 0px !important;
padding: 0 !important;
}
+
+br.big {
+ display: block;
+ content: "";
+ margin-top: 1rem;
+}
+
+++ /dev/null
-/*!\r
- * Selectr 2.4.13\r
- * http://mobius.ovh/docs/selectr\r
- *\r
- * Released under the MIT license\r
- */\r
-.selectr-container li,.selectr-option,.selectr-tag{list-style:none}.selectr-container{position:relative}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:'';-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:none;border-radius:10px;background:#acb7bf}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active,.selectr-input-container.active .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:' ';background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;}.input-tag,.taggable .selectr-label{width:auto}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;cursor:pointer;font-weight:400}.has-selected .selectr-placeholder,.selectr-empty,.selectr-option.excluded{display:none}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px}.selectr-tag-input{border:none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:'';-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running spin;-webkit-animation:.5s linear 0s normal forwards infinite running spin;animation:.5s linear 0s normal forwards infinite running spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1}\r
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({
});
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();
});
},
"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",
"@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",
"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",
"tippy.js": "^6.1.1",
"toastify-js": "^1.7.0",
"tributejs": "^5.1.3",
- "twemoji": "^12.1.2",
"ws": "^7.2.3"
},
"devDependencies": {
"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": {
--- /dev/null
+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 });
+ }
+}
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';
toast,
setupTribute,
wsJsonToRes,
- emojiPicker,
pictrsDeleteToast,
} from '../utils';
import { WebSocketService, UserService } from '../services';
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;
onReplyCancel?(): any;
edit?: boolean;
disabled?: boolean;
+ focus?: boolean;
}
interface CommentFormState {
super(props, context);
this.tribute = setupTribute();
- this.setupEmojiPicker();
this.state = this.emptyState;
}
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() {
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}
/>
- )}
- </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 ? (
- <svg class="icon icon-spinner spin">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- ) : (
- <span>{this.state.buttonTitle}</span>
+ {this.state.previewMode && (
+ <div
+ className="card card-body md-div"
+ dangerouslySetInnerHTML={mdToHtml(
+ this.state.commentForm.content
+ )}
+ />
)}
- </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 && (
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-sm-12">
<button
- type="button"
+ type="submit"
class="btn btn-sm btn-secondary mr-2"
- onClick={linkEvent(this, this.handleReplyCancel)}
+ disabled={this.props.disabled || this.state.loading}
>
- {i18n.t('cancel')}
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ <span>{this.state.buttonTitle}</span>
+ )}
</button>
- )}
- <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')}
+ {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-image"></use>
+ <use xlinkHref="#icon-help-circle"></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>
+ </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>
+ )}
+ </div>
</div>
+ </form>
+ ) : (
+ <div class="alert alert-light" role="alert">
+ <svg class="icon icon-inline mr-2">
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ <T i18nKey="must_login" class="d-inline">
+ #
+ <Link class="alert-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;
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);
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 {
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')}
{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 ? (
<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}
>
<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} />
edit
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
+ focus
/>
)}
{!this.state.showEdit && !this.state.collapsed && (
node={node}
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
+ focus
/>
)}
{node.children && !this.state.collapsed && (
GetSiteResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
-import { wsJsonToRes, toast } from '../utils';
+import { wsJsonToRes, toast, getPageFromProps } from '../utils';
import { CommunityLink } from './community-link';
import { i18n } from '../i18next';
loading: boolean;
}
+interface CommunitiesProps {
+ page: number;
+}
+
export class Communities extends Component<any, CommunitiesState> {
private subscription: Subscription;
private emptyState: CommunitiesState = {
communities: [],
loading: true,
- page: this.getPageFromProps(this.props),
+ page: getPageFromProps(this.props),
};
constructor(props: any, context: any) {
WebSocketService.Instance.getSite();
}
- getPageFromProps(props: any): number {
- return props.match.params.page ? Number(props.match.params.page) : 1;
- }
-
componentWillUnmount() {
this.subscription.unsubscribe();
}
- // Necessary for back button for some reason
- componentWillReceiveProps(nextProps: any) {
- if (nextProps.history.action == 'POP') {
- this.state = this.emptyState;
- this.state.page = this.getPageFromProps(nextProps);
+ static getDerivedStateFromProps(props: any): CommunitiesProps {
+ return {
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: CommunitiesState) {
+ if (lastState.page !== this.state.page) {
+ this.setState({ loading: true });
this.refetch();
}
}
</button>
)}
- {this.state.communities.length == communityLimit && (
+ {this.state.communities.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
);
}
- updateUrl() {
- this.props.history.push(`/communities/page/${this.state.page}`);
+ updateUrl(paramUpdates: CommunitiesProps) {
+ const page = paramUpdates.page || this.state.page;
+ this.props.history.push(`/communities/page/${page}`);
}
nextPage(i: Communities) {
- i.state.page++;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
+ i.updateUrl({ page: i.state.page + 1 });
}
prevPage(i: Communities) {
- i.state.page--;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
+ i.updateUrl({ page: i.state.page - 1 });
}
handleUnsubscribe(communityId: number) {
site: Site;
}
+interface CommunityProps {
+ dataType: DataType;
+ sort: SortType;
+ page: number;
+}
+
+interface UrlParams {
+ dataType?: string;
+ sort?: string;
+ page?: number;
+}
+
export class Community extends Component<any, State> {
private subscription: Subscription;
private emptyState: State = {
this.subscription.unsubscribe();
}
- // Necessary for back button for some reason
- componentWillReceiveProps(nextProps: any) {
+ static getDerivedStateFromProps(props: any): CommunityProps {
+ return {
+ dataType: getDataTypeFromProps(props),
+ sort: getSortTypeFromProps(props),
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: State) {
if (
- nextProps.history.action == 'POP' ||
- nextProps.history.action == 'PUSH'
+ lastState.dataType !== this.state.dataType ||
+ lastState.sort !== this.state.sort ||
+ lastState.page !== this.state.page
) {
- this.state.dataType = getDataTypeFromProps(nextProps);
- this.state.sort = getSortTypeFromProps(nextProps);
- this.state.page = getPageFromProps(nextProps);
- this.setState(this.state);
+ this.setState({ loading: true });
this.fetchData();
}
}
{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)}
}
nextPage(i: Community) {
- i.state.page++;
- i.setState(i.state);
- i.updateUrl();
- i.fetchData();
+ i.updateUrl({ page: i.state.page + 1 });
window.scrollTo(0, 0);
}
prevPage(i: Community) {
- i.state.page--;
- i.setState(i.state);
- i.updateUrl();
- i.fetchData();
+ i.updateUrl({ page: i.state.page - 1 });
window.scrollTo(0, 0);
}
handleSortChange(val: SortType) {
- this.state.sort = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
window.scrollTo(0, 0);
}
handleDataTypeChange(val: DataType) {
- this.state.dataType = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
window.scrollTo(0, 0);
}
- updateUrl() {
- let dataTypeStr = DataType[this.state.dataType].toLowerCase();
- let sortStr = SortType[this.state.sort].toLowerCase();
+ updateUrl(paramUpdates: UrlParams) {
+ const dataTypeStr =
+ paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
+ const sortStr =
+ paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+ const page = paramUpdates.page || this.state.page;
this.props.history.push(
- `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}`
+ `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
);
}
GetSiteResponse,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
import { i18n } from '../i18next';
interface 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(
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,
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(
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,
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(
this.state = this.emptyState;
}
+ static getDerivedStateFromProps(props: any): DataTypeSelectProps {
+ return {
+ type_: props.type_,
+ };
+ }
+
render() {
return (
<div class="btn-group btn-group-toggle">
{i18n.t('posts')}
</label>
<label
- className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
- DataType.Comment && 'active'}`}
+ className={`pointer btn btn-sm btn-secondary ${
+ this.state.type_ == DataType.Comment && 'active'
+ }`}
>
<input
type="radio"
}
handleTypeChange(i: DataTypeSelect, event: any) {
- i.state.type_ = Number(event.target.value);
- i.setState(i.state);
- i.props.onChange(i.state.type_);
+ i.props.onChange(Number(event.target.value));
}
}
nodes={[{ comment: i }]}
noIndent
markable
+ showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
/>
nodes={commentsToFlatNodes(this.state.replies)}
noIndent
markable
+ showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
/>
nodes={[{ comment: mention }]}
noIndent
markable
+ showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
/>
{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>
);
}
}
sendUnreadCount() {
- let count =
+ UserService.Instance.user.unreadCount = this.unreadCount();
+ UserService.Instance.sub.next({
+ user: UserService.Instance.user,
+ });
+ }
+
+ unreadCount(): number {
+ return (
this.state.replies.filter(r => !r.read).length +
this.state.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.sub.next({
- user: UserService.Instance.user,
- });
+ ).length
+ );
}
}
this.state = this.emptyState;
}
+ static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
+ return {
+ type_: props.type_,
+ };
+ }
+
render() {
return (
<div class="btn-group btn-group-toggle">
{i18n.t('subscribed')}
</label>
<label
- className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
- ListingType.All && 'active'}`}
+ className={`pointer btn btn-sm btn-secondary ${
+ this.state.type_ == ListingType.All && 'active'
+ }`}
>
<input
type="radio"
}
handleTypeChange(i: ListingTypeSelect, event: any) {
- i.state.type_ = Number(event.target.value);
- i.setState(i.state);
- i.props.onChange(i.state.type_);
+ i.props.onChange(Number(event.target.value));
}
}
class="form-control"
required
/>
- <button
- type="button"
- disabled={!validEmail(this.state.loginForm.username_or_email)}
- onClick={linkEvent(this, this.handlePasswordReset)}
- className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
- >
- {i18n.t('forgot_password')}
- </button>
+ {validEmail(this.state.loginForm.username_or_email) && (
+ <button
+ type="button"
+ onClick={linkEvent(this, this.handlePasswordReset)}
+ className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
+ >
+ {i18n.t('forgot_password')}
+ </button>
+ )}
</div>
</div>
<div class="form-group row">
onInput={linkEvent(this, this.handleRegisterEmailChange)}
minLength={3}
/>
+ {!validEmail(this.state.registerForm.email) && (
+ <div class="mt-2 mb-0 alert alert-light" role="alert">
+ <svg class="icon icon-inline mr-2">
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ {i18n.t('no_password_reset')}
+ </div>
+ )}
</div>
</div>
page: number;
}
+interface MainProps {
+ listingType: ListingType;
+ dataType: DataType;
+ sort: SortType;
+ page: number;
+}
+
+interface UrlParams {
+ listingType?: string;
+ dataType?: string;
+ sort?: string;
+ page?: number;
+}
+
export class Main extends Component<any, MainState> {
private subscription: Subscription;
private emptyState: MainState = {
this.subscription.unsubscribe();
}
- // Necessary for back button for some reason
- componentWillReceiveProps(nextProps: any) {
+ static getDerivedStateFromProps(props: any): MainProps {
+ return {
+ listingType: getListingTypeFromProps(props),
+ dataType: getDataTypeFromProps(props),
+ sort: getSortTypeFromProps(props),
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: MainState) {
if (
- nextProps.history.action == 'POP' ||
- nextProps.history.action == 'PUSH'
+ lastState.listingType !== this.state.listingType ||
+ lastState.dataType !== this.state.dataType ||
+ lastState.sort !== this.state.sort ||
+ lastState.page !== this.state.page
) {
- this.state.listingType = getListingTypeFromProps(nextProps);
- this.state.dataType = getDataTypeFromProps(nextProps);
- this.state.sort = getSortTypeFromProps(nextProps);
- this.state.page = getPageFromProps(nextProps);
- this.setState(this.state);
+ this.setState({ loading: true });
this.fetchData();
}
}
);
}
- updateUrl() {
- let listingTypeStr = ListingType[this.state.listingType].toLowerCase();
- let dataTypeStr = DataType[this.state.dataType].toLowerCase();
- let sortStr = SortType[this.state.sort].toLowerCase();
+ updateUrl(paramUpdates: UrlParams) {
+ const listingTypeStr =
+ paramUpdates.listingType ||
+ ListingType[this.state.listingType].toLowerCase();
+ const dataTypeStr =
+ paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
+ const sortStr =
+ paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+ const page = paramUpdates.page || this.state.page;
this.props.history.push(
- `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}`
+ `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
);
}
#
</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>
{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)}
}
nextPage(i: Main) {
- i.state.page++;
- i.state.loading = true;
- i.setState(i.state);
- i.updateUrl();
- i.fetchData();
+ i.updateUrl({ page: i.state.page + 1 });
window.scrollTo(0, 0);
}
prevPage(i: Main) {
- i.state.page--;
- i.state.loading = true;
- i.setState(i.state);
- i.updateUrl();
- i.fetchData();
+ i.updateUrl({ page: i.state.page - 1 });
window.scrollTo(0, 0);
}
handleSortChange(val: SortType) {
- this.state.sort = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
window.scrollTo(0, 0);
}
handleListingTypeChange(val: ListingType) {
- this.state.listingType = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ listingType: ListingType[val].toLowerCase(), page: 1 });
window.scrollTo(0, 0);
}
handleDataTypeChange(val: DataType) {
- this.state.dataType = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
window.scrollTo(0, 0);
}
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;
private id = `post-form-${randomStr()}`;
private tribute: Tribute;
private subscription: Subscription;
+ private choices: Choices;
private emptyState: PostFormState = {
postForm: {
name: null,
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.tribute = setupTribute();
- this.setupEmojiPicker();
this.state = this.emptyState;
componentWillUnmount() {
this.subscription.unsubscribe();
+ this.choices && this.choices.destroy();
window.onbeforeunload = null;
}
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">
<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 && (
);
}
- 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();
});
}
- handleEmojiPickerClick(_i: PostForm, event: any) {
- emojiPicker.togglePicker(event.target);
- }
-
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (msg.error) {
// 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;
setupTippy,
hostname,
previewLines,
+ toast,
} from '../utils';
import { i18n } from '../i18next';
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')}
}
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) {
}
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) {
CommentForm as CommentFormI,
CommentResponse,
CommentSortType,
+ CommentViewType,
CommunityUser,
CommunityResponse,
CommentNode as CommentNodeI,
post: PostI;
comments: Array<Comment>;
commentSort: CommentSortType;
+ commentViewType: CommentViewType;
community: Community;
moderators: Array<CommunityUser>;
online: number;
post: null,
comments: [],
commentSort: CommentSortType.Hot,
+ commentViewType: CommentViewType.Tree,
community: null,
moderators: [],
online: null,
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>
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>
);
}
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);
}
createCommentLikeRes,
createPostLikeFindRes,
commentsToFlatNodes,
+ getPageFromProps,
} from '../utils';
import { PostListing } from './post-listing';
import { UserListing } from './user-listing';
searchResponse: SearchResponse;
loading: boolean;
site: Site;
+ searchText: string;
+}
+
+interface SearchProps {
+ q: string;
+ type_: SearchType;
+ sort: SortType;
+ page: number;
+}
+
+interface UrlParams {
+ q?: string;
+ type_?: string;
+ sort?: string;
+ page?: number;
}
export class Search extends Component<any, SearchState> {
private subscription: Subscription;
private emptyState: SearchState = {
- q: this.getSearchQueryFromProps(this.props),
- type_: this.getSearchTypeFromProps(this.props),
- sort: this.getSortTypeFromProps(this.props),
- page: this.getPageFromProps(this.props),
+ q: Search.getSearchQueryFromProps(this.props),
+ type_: Search.getSearchTypeFromProps(this.props),
+ sort: Search.getSortTypeFromProps(this.props),
+ page: getPageFromProps(this.props),
+ searchText: Search.getSearchQueryFromProps(this.props),
searchResponse: {
type_: null,
posts: [],
},
};
- getSearchQueryFromProps(props: any): string {
+ static getSearchQueryFromProps(props: any): string {
return props.match.params.q ? props.match.params.q : '';
}
- getSearchTypeFromProps(props: any): SearchType {
+ static getSearchTypeFromProps(props: any): SearchType {
return props.match.params.type
? routeSearchTypeToEnum(props.match.params.type)
: SearchType.All;
}
- getSortTypeFromProps(props: any): SortType {
+ static getSortTypeFromProps(props: any): SortType {
return props.match.params.sort
? routeSortTypeToEnum(props.match.params.sort)
: SortType.TopAll;
}
- getPageFromProps(props: any): number {
- return props.match.params.page ? Number(props.match.params.page) : 1;
- }
-
constructor(props: any, context: any) {
super(props, context);
this.subscription.unsubscribe();
}
- // Necessary for back button for some reason
- componentWillReceiveProps(nextProps: any) {
+ static getDerivedStateFromProps(props: any): SearchProps {
+ return {
+ q: Search.getSearchQueryFromProps(props),
+ type_: Search.getSearchTypeFromProps(props),
+ sort: Search.getSortTypeFromProps(props),
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: SearchState) {
if (
- nextProps.history.action == 'POP' ||
- nextProps.history.action == 'PUSH'
+ lastState.q !== this.state.q ||
+ lastState.type_ !== this.state.type_ ||
+ lastState.sort !== this.state.sort ||
+ lastState.page !== this.state.page
) {
- this.state.q = this.getSearchQueryFromProps(nextProps);
- this.state.type_ = this.getSearchTypeFromProps(nextProps);
- this.state.sort = this.getSortTypeFromProps(nextProps);
- this.state.page = this.getPageFromProps(nextProps);
- this.setState(this.state);
+ this.setState({ loading: true, searchText: this.state.q });
this.search();
}
}
{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>
);
<input
type="text"
class="form-control mr-2"
- value={this.state.q}
+ value={this.state.searchText}
placeholder={`${i18n.t('search')}...`}
onInput={linkEvent(this, this.handleQChange)}
required
{i.type_ == 'users' && (
<div>
<span>
+ @
<UserListing
user={{
name: (i.data as UserView).name,
}}
/>
</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>
<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>
))}
{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
);
}
nextPage(i: Search) {
- i.state.page++;
- i.setState(i.state);
- i.updateUrl();
- i.search();
+ i.updateUrl({ page: i.state.page + 1 });
}
prevPage(i: Search) {
- i.state.page--;
- i.setState(i.state);
- i.updateUrl();
- i.search();
+ i.updateUrl({ page: i.state.page - 1 });
}
search() {
}
handleSortChange(val: SortType) {
- this.state.sort = val;
- this.state.page = 1;
- this.setState(this.state);
- this.updateUrl();
+ this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
}
handleTypeChange(i: Search, event: any) {
- i.state.type_ = Number(event.target.value);
- i.state.page = 1;
- i.setState(i.state);
- i.updateUrl();
+ i.updateUrl({
+ type_: SearchType[Number(event.target.value)].toLowerCase(),
+ page: 1,
+ });
}
handleSearchSubmit(i: Search, event: any) {
event.preventDefault();
- i.state.loading = true;
- i.search();
- i.setState(i.state);
- i.updateUrl();
+ i.updateUrl({
+ q: i.state.searchText,
+ type_: SearchType[i.state.type_].toLowerCase(),
+ sort: SortType[i.state.sort].toLowerCase(),
+ page: i.state.page,
+ });
}
handleQChange(i: Search, event: any) {
- i.state.q = event.target.value;
- i.setState(i.state);
+ i.setState({ searchText: event.target.value });
}
- updateUrl() {
- let typeStr = SearchType[this.state.type_].toLowerCase();
- let sortStr = SortType[this.state.sort].toLowerCase();
+ updateUrl(paramUpdates: UrlParams) {
+ const qStr = paramUpdates.q || this.state.q;
+ const typeStr =
+ paramUpdates.type_ || SearchType[this.state.type_].toLowerCase();
+ const sortStr =
+ paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+ const page = paramUpdates.page || this.state.page;
this.props.history.push(
- `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
+ `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
);
}
this.state = this.emptyState;
}
+ static getDerivedStateFromProps(props: any): SortSelectState {
+ return {
+ sort: props.sort,
+ };
+ }
+
render() {
return (
<>
}
handleSortChange(i: SortSelect, event: any) {
- i.state.sort = Number(event.target.value);
- i.setState(i.state);
- i.props.onChange(i.state.sort);
+ i.props.onChange(event.target.value);
}
}
<symbol id="icon-spinner" viewBox="0 0 32 32">
<path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
</symbol>
+ <symbol id="icon-cake" viewBox="0 0 24 24">
+ <path d="M 23.296875 22.394531 L 22.082031 22.394531 L 22.082031 17.007812 C 22.453125 16.699219 22.664062 16.261719 22.664062 15.796875 L 22.664062 13.984375 C 22.664062 12.996094 21.785156 12.191406 20.703125 12.191406 L 19.785156 12.191406 L 19.785156 7.785156 C 19.785156 7.050781 19.1875 6.449219 18.449219 6.449219 L 18.367188 6.449219 L 18.367188 5.96875 C 19.199219 5.675781 19.796875 4.882812 19.796875 3.957031 C 19.796875 3.644531 19.703125 3.117188 18.996094 1.800781 C 18.632812 1.121094 18.273438 0.550781 18.257812 0.527344 C 18.128906 0.320312 17.90625 0.199219 17.664062 0.199219 C 17.421875 0.199219 17.199219 0.320312 17.070312 0.527344 C 17.054688 0.550781 16.695312 1.121094 16.332031 1.800781 C 15.621094 3.117188 15.53125 3.644531 15.53125 3.957031 C 15.53125 4.882812 16.128906 5.675781 16.960938 5.96875 L 16.960938 6.449219 L 16.878906 6.449219 C 16.140625 6.449219 15.542969 7.050781 15.542969 7.785156 L 15.542969 12.191406 L 14.121094 12.191406 L 14.121094 7.785156 C 14.121094 7.050781 13.523438 6.449219 12.785156 6.449219 L 12.703125 6.449219 L 12.703125 5.96875 C 13.535156 5.675781 14.132812 4.882812 14.132812 3.957031 C 14.132812 3.644531 14.039062 3.117188 13.332031 1.800781 C 12.96875 1.121094 12.609375 0.550781 12.59375 0.527344 C 12.464844 0.320312 12.242188 0.199219 12 0.199219 C 11.757812 0.199219 11.535156 0.320312 11.40625 0.527344 C 11.390625 0.550781 11.03125 1.121094 10.667969 1.800781 C 9.960938 3.117188 9.867188 3.644531 9.867188 3.957031 C 9.867188 4.882812 10.464844 5.675781 11.296875 5.96875 L 11.296875 6.449219 L 11.214844 6.449219 C 10.476562 6.449219 9.878906 7.050781 9.878906 7.785156 L 9.878906 12.191406 L 8.457031 12.191406 L 8.457031 7.785156 C 8.457031 7.050781 7.859375 6.449219 7.121094 6.449219 L 7.039062 6.449219 L 7.039062 5.96875 C 7.871094 5.675781 8.46875 4.882812 8.46875 3.957031 C 8.46875 3.644531 8.378906 3.117188 7.667969 1.800781 C 7.304688 1.121094 6.945312 0.550781 6.929688 0.527344 C 6.800781 0.320312 6.578125 0.199219 6.335938 0.199219 C 6.09375 0.199219 5.871094 0.320312 5.742188 0.527344 C 5.726562 0.550781 5.367188 1.121094 5.003906 1.800781 C 4.296875 3.117188 4.203125 3.644531 4.203125 3.957031 C 4.203125 4.882812 4.800781 5.675781 5.632812 5.96875 L 5.632812 6.449219 L 5.550781 6.449219 C 4.8125 6.449219 4.214844 7.050781 4.214844 7.785156 L 4.214844 12.191406 L 3.296875 12.191406 C 2.214844 12.191406 1.335938 12.996094 1.335938 13.984375 L 1.335938 15.796875 C 1.335938 16.261719 1.546875 16.699219 1.917969 17.007812 L 1.917969 22.394531 L 0.703125 22.394531 C 0.316406 22.394531 0 22.710938 0 23.097656 C 0 23.488281 0.316406 23.800781 0.703125 23.800781 L 23.296875 23.800781 C 23.683594 23.800781 24 23.488281 24 23.097656 C 24 22.710938 23.683594 22.394531 23.296875 22.394531 Z M 16.9375 3.957031 C 16.941406 3.730469 17.246094 3.054688 17.664062 2.289062 C 18.082031 3.054688 18.382812 3.730469 18.390625 3.957031 C 18.390625 4.355469 18.0625 4.679688 17.664062 4.679688 C 17.265625 4.679688 16.9375 4.355469 16.9375 3.957031 Z M 16.949219 7.855469 L 18.378906 7.855469 L 18.378906 12.1875 L 16.949219 12.1875 Z M 11.273438 3.957031 C 11.277344 3.730469 11.582031 3.054688 12 2.289062 C 12.417969 3.054688 12.722656 3.730469 12.726562 3.957031 C 12.726562 4.355469 12.398438 4.679688 12 4.679688 C 11.601562 4.679688 11.273438 4.355469 11.273438 3.957031 Z M 11.285156 7.855469 L 12.714844 7.855469 L 12.714844 12.1875 L 11.285156 12.1875 Z M 5.609375 3.957031 C 5.613281 3.730469 5.917969 3.054688 6.335938 2.289062 C 6.753906 3.054688 7.058594 3.730469 7.0625 3.957031 C 7.0625 4.355469 6.734375 4.679688 6.335938 4.679688 C 5.9375 4.679688 5.609375 4.355469 5.609375 3.957031 Z M 5.621094 7.855469 L 7.050781 7.855469 L 7.050781 12.1875 L 5.621094 12.1875 Z M 20.675781 22.394531 L 3.324219 22.394531 L 3.324219 17.414062 C 3.433594 17.398438 3.546875 17.378906 3.652344 17.347656 L 5.429688 16.820312 C 6.453125 16.515625 7.582031 16.515625 8.609375 16.820312 L 10.011719 17.234375 C 10.652344 17.425781 11.324219 17.519531 12 17.519531 C 12.675781 17.519531 13.347656 17.425781 13.988281 17.234375 L 15.390625 16.820312 C 16.417969 16.515625 17.546875 16.515625 18.570312 16.820312 L 20.347656 17.347656 C 20.453125 17.378906 20.5625 17.398438 20.675781 17.414062 Z M 21.257812 15.796875 C 21.257812 15.855469 21.210938 15.902344 21.171875 15.933594 C 21.082031 16 20.925781 16.050781 20.746094 15.996094 L 18.972656 15.472656 C 17.6875 15.09375 16.273438 15.09375 14.992188 15.472656 L 13.589844 15.886719 C 12.566406 16.191406 11.433594 16.191406 10.410156 15.886719 L 9.007812 15.472656 C 8.367188 15.28125 7.691406 15.1875 7.019531 15.1875 C 6.34375 15.1875 5.671875 15.28125 5.027344 15.472656 L 3.253906 15.996094 C 3.074219 16.050781 2.917969 16 2.828125 15.933594 C 2.789062 15.902344 2.742188 15.855469 2.742188 15.796875 L 2.742188 13.984375 C 2.742188 13.800781 2.96875 13.597656 3.296875 13.597656 L 20.703125 13.597656 C 21.03125 13.597656 21.257812 13.800781 21.257812 13.984375 Z M 21.257812 15.796875 " />
+ </symbol>
</defs>
</svg>
);
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { WebSocketService, UserService } from '../services';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take, last } from 'rxjs/operators';
+import { i18n } from '../i18next';
+import {
+ UserOperation,
+ Post,
+ Comment,
+ CommunityUser,
+ SortType,
+ UserDetailsResponse,
+ UserView,
+ WebSocketJsonResponse,
+ UserDetailsView,
+ CommentResponse,
+ BanUserResponse,
+ PostResponse,
+ AddAdminResponse,
+} from '../interfaces';
+import {
+ wsJsonToRes,
+ toast,
+ commentsToFlatNodes,
+ setupTippy,
+ editCommentRes,
+ saveCommentRes,
+ createCommentLikeRes,
+ createPostLikeFindRes,
+} from '../utils';
+import { PostListing } from './post-listing';
+import { CommentNodes } from './comment-nodes';
+
+interface UserDetailsProps {
+ username?: string;
+ user_id?: number;
+ page: number;
+ limit: number;
+ sort: string;
+ enableDownvotes: boolean;
+ enableNsfw: boolean;
+ view: UserDetailsView;
+ onPageChange(page: number): number | any;
+}
+
+interface UserDetailsState {
+ follows: Array<CommunityUser>;
+ moderates: Array<CommunityUser>;
+ comments: Array<Comment>;
+ posts: Array<Post>;
+ saved?: Array<Post>;
+ admins: Array<UserView>;
+}
+
+export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
+ private subscription: Subscription;
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = {
+ follows: [],
+ moderates: [],
+ comments: [],
+ posts: [],
+ saved: [],
+ admins: [],
+ };
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ componentDidMount() {
+ this.fetchUserData();
+ }
+
+ componentDidUpdate(lastProps: UserDetailsProps) {
+ for (const key of Object.keys(lastProps)) {
+ if (lastProps[key] !== this.props[key]) {
+ this.fetchUserData();
+ break;
+ }
+ }
+ setupTippy();
+ }
+
+ fetchUserData() {
+ WebSocketService.Instance.getUserDetails({
+ user_id: this.props.user_id,
+ username: this.props.username,
+ sort: this.props.sort,
+ saved_only: this.props.view === UserDetailsView.Saved,
+ page: this.props.page,
+ limit: this.props.limit,
+ });
+ }
+
+ render() {
+ return (
+ <div>
+ {this.viewSelector(this.props.view)}
+ {this.paginator()}
+ </div>
+ );
+ }
+
+ viewSelector(view: UserDetailsView) {
+ if (view === UserDetailsView.Overview || view === UserDetailsView.Saved) {
+ return this.overview();
+ }
+ if (view === UserDetailsView.Comments) {
+ return this.comments();
+ }
+ if (view === UserDetailsView.Posts) {
+ return this.posts();
+ }
+ }
+
+ overview() {
+ const comments = this.state.comments.map((c: Comment) => {
+ return { type: 'comments', data: c };
+ });
+ const posts = this.state.posts.map((p: Post) => {
+ return { type: 'posts', data: p };
+ });
+
+ const combined: Array<{ type: string; data: Comment | Post }> = [
+ ...comments,
+ ...posts,
+ ];
+
+ // Sort it
+ if (SortType[this.props.sort] === SortType.New) {
+ combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
+ } else {
+ combined.sort((a, b) => b.data.score - a.data.score);
+ }
+
+ return (
+ <div>
+ {combined.map(i => (
+ <div>
+ {i.type === 'posts' ? (
+ <PostListing
+ post={i.data as Post}
+ admins={this.state.admins}
+ showCommunity
+ enableDownvotes={this.props.enableDownvotes}
+ enableNsfw={this.props.enableNsfw}
+ />
+ ) : (
+ <CommentNodes
+ nodes={[{ comment: i.data as Comment }]}
+ admins={this.state.admins}
+ noIndent
+ showContext
+ enableDownvotes={this.props.enableDownvotes}
+ />
+ )}
+ </div>
+ ))}
+ </div>
+ );
+ }
+
+ comments() {
+ return (
+ <div>
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ admins={this.state.admins}
+ noIndent
+ showContext
+ enableDownvotes={this.props.enableDownvotes}
+ />
+ </div>
+ );
+ }
+
+ posts() {
+ return (
+ <div>
+ {this.state.posts.map(post => (
+ <PostListing
+ post={post}
+ admins={this.state.admins}
+ showCommunity
+ enableDownvotes={this.props.enableDownvotes}
+ enableNsfw={this.props.enableNsfw}
+ />
+ ))}
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="my-2">
+ {this.props.page > 1 && (
+ <button
+ class="btn btn-sm btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </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>
+ );
+ }
+
+ nextPage(i: UserDetails) {
+ i.props.onPageChange(i.props.page + 1);
+ }
+
+ prevPage(i: UserDetails) {
+ i.props.onPageChange(i.props.page - 1);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ const res = wsJsonToRes(msg);
+
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ if (msg.error == 'couldnt_find_that_username_or_email') {
+ this.context.router.history.push('/');
+ }
+ return;
+ } else if (msg.reconnect) {
+ this.fetchUserData();
+ } else if (res.op == UserOperation.GetUserDetails) {
+ const data = res.data as UserDetailsResponse;
+ this.setState({
+ comments: data.comments,
+ follows: data.follows,
+ moderates: data.moderates,
+ posts: data.posts,
+ admins: data.admins,
+ });
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ const data = res.data as CommentResponse;
+ createCommentLikeRes(data, this.state.comments);
+ this.setState({
+ comments: this.state.comments,
+ });
+ } else if (res.op == UserOperation.EditComment) {
+ const data = res.data as CommentResponse;
+ editCommentRes(data, this.state.comments);
+ this.setState({
+ comments: this.state.comments,
+ });
+ } else if (res.op == UserOperation.CreateComment) {
+ const data = res.data as CommentResponse;
+ if (
+ UserService.Instance.user &&
+ data.comment.creator_id == UserService.Instance.user.id
+ ) {
+ toast(i18n.t('reply_sent'));
+ }
+ } else if (res.op == UserOperation.SaveComment) {
+ const data = res.data as CommentResponse;
+ saveCommentRes(data, this.state.comments);
+ this.setState({
+ comments: this.state.comments,
+ });
+ } else if (res.op == UserOperation.CreatePostLike) {
+ const data = res.data as PostResponse;
+ createPostLikeFindRes(data, this.state.posts);
+ this.setState({
+ posts: this.state.posts,
+ });
+ } else if (res.op == UserOperation.BanUser) {
+ const data = res.data as BanUserResponse;
+ this.state.comments
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
+ this.state.posts
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
+ this.setState({
+ posts: this.state.posts,
+ comments: this.state.comments,
+ });
+ } else if (res.op == UserOperation.AddAdmin) {
+ const data = res.data as AddAdminResponse;
+ this.setState({
+ admins: data.admins,
+ });
+ }
+ }
+}
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;
avatar?: string;
local?: boolean;
actor_id?: string;
+ published?: string;
}
interface UserListingProps {
}
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_} />}
+ </>
);
}
}
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
- Post,
- Comment,
CommunityUser,
- GetUserDetailsForm,
SortType,
ListingType,
- UserDetailsResponse,
UserView,
- CommentResponse,
UserSettingsForm,
LoginResponse,
- BanUserResponse,
- AddAdminResponse,
DeleteAccountForm,
- PostResponse,
WebSocketJsonResponse,
GetSiteResponse,
Site,
+ UserDetailsView,
+ UserDetailsResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
languages,
showAvatars,
toast,
- editCommentRes,
- saveCommentRes,
- createCommentLikeRes,
- createPostLikeFindRes,
- commentsToFlatNodes,
setupTippy,
} from '../utils';
-import { PostListing } from './post-listing';
import { UserListing } from './user-listing';
import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select';
-import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time';
import { i18n } from '../i18next';
-
-enum View {
- Overview,
- Comments,
- Posts,
- Saved,
-}
+import moment from 'moment';
+import { UserDetails } from './user-details';
interface UserState {
user: UserView;
username: string;
follows: Array<CommunityUser>;
moderates: Array<CommunityUser>;
- comments: Array<Comment>;
- posts: Array<Post>;
- saved?: Array<Post>;
- admins: Array<UserView>;
- view: View;
+ view: UserDetailsView;
sort: SortType;
page: number;
loading: boolean;
site: Site;
}
+interface UserProps {
+ view: UserDetailsView;
+ sort: SortType;
+ page: number;
+ user_id: number | null;
+ username: string;
+}
+
+interface UrlParams {
+ view?: string;
+ sort?: string;
+ page?: number;
+}
+
export class User extends Component<any, UserState> {
private subscription: Subscription;
private emptyState: UserState = {
username: null,
follows: [],
moderates: [],
- comments: [],
- posts: [],
- admins: [],
- loading: true,
+ loading: false,
avatarLoading: false,
- view: this.getViewFromProps(this.props),
- sort: this.getSortTypeFromProps(this.props),
- page: this.getPageFromProps(this.props),
+ view: User.getViewFromProps(this.props.match.view),
+ sort: User.getSortTypeFromProps(this.props.match.sort),
+ page: User.getPageFromProps(this.props.match.page),
userSettingsForm: {
show_nsfw: null,
theme: null,
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this
);
+ this.handlePageChange = this.handlePageChange.bind(this);
- this.state.user_id = Number(this.props.match.params.id);
+ this.state.user_id = Number(this.props.match.params.id) || null;
this.state.username = this.props.match.params.username;
this.subscription = WebSocketService.Instance.subject
() => console.log('complete')
);
- this.refetch();
WebSocketService.Instance.getSite();
}
);
}
- getViewFromProps(props: any): View {
- return props.match.params.view
- ? View[capitalizeFirstLetter(props.match.params.view)]
- : View.Overview;
+ static getViewFromProps(view: any): UserDetailsView {
+ return view
+ ? UserDetailsView[capitalizeFirstLetter(view)]
+ : UserDetailsView.Overview;
}
- getSortTypeFromProps(props: any): SortType {
- return props.match.params.sort
- ? routeSortTypeToEnum(props.match.params.sort)
- : SortType.New;
+ static getSortTypeFromProps(sort: any): SortType {
+ return sort ? routeSortTypeToEnum(sort) : SortType.New;
}
- getPageFromProps(props: any): number {
- return props.match.params.page ? Number(props.match.params.page) : 1;
+ static getPageFromProps(page: any): number {
+ return page ? Number(page) : 1;
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
- // Necessary for back button for some reason
- componentWillReceiveProps(nextProps: any) {
- if (
- nextProps.history.action == 'POP' ||
- nextProps.history.action == 'PUSH'
- ) {
- this.state.view = this.getViewFromProps(nextProps);
- this.state.sort = this.getSortTypeFromProps(nextProps);
- this.state.page = this.getPageFromProps(nextProps);
- this.setState(this.state);
- this.refetch();
- }
+ static getDerivedStateFromProps(props: any): UserProps {
+ return {
+ view: this.getViewFromProps(props.match.params.view),
+ sort: this.getSortTypeFromProps(props.match.params.sort),
+ page: this.getPageFromProps(props.match.params.page),
+ user_id: Number(props.match.params.id) || null,
+ username: props.match.params.username,
+ };
}
componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
// Couldnt get a refresh working. This does for now.
location.reload();
}
+ document.title = `/u/${this.state.username} - ${this.state.site.name}`;
+ setupTippy();
}
render() {
class="rounded-circle mr-2"
/>
)}
- <span>/u/{this.state.user.name}</span>
+ <span>/u/{this.state.username}</span>
</h5>
{this.selects()}
- {this.state.view == View.Overview && this.overview()}
- {this.state.view == View.Comments && this.comments()}
- {this.state.view == View.Posts && this.posts()}
- {this.state.view == View.Saved && this.overview()}
- {this.paginator()}
+ <UserDetails
+ user_id={this.state.user_id}
+ username={this.state.username}
+ sort={SortType[this.state.sort]}
+ page={this.state.page}
+ limit={fetchLimit}
+ enableDownvotes={this.state.site.enable_downvotes}
+ enableNsfw={this.state.site.enable_nsfw}
+ view={this.state.view}
+ onPageChange={this.handlePageChange}
+ />
</div>
<div class="col-12 col-md-4">
{this.userInfo()}
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Overview && 'active'}
+ ${this.state.view == UserDetailsView.Overview && 'active'}
`}
>
<input
type="radio"
- value={View.Overview}
- checked={this.state.view == View.Overview}
+ value={UserDetailsView.Overview}
+ checked={this.state.view === UserDetailsView.Overview}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('overview')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Comments && 'active'}
+ ${this.state.view == UserDetailsView.Comments && 'active'}
`}
>
<input
type="radio"
- value={View.Comments}
- checked={this.state.view == View.Comments}
+ value={UserDetailsView.Comments}
+ checked={this.state.view == UserDetailsView.Comments}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('comments')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Posts && 'active'}
+ ${this.state.view == UserDetailsView.Posts && 'active'}
`}
>
<input
type="radio"
- value={View.Posts}
- checked={this.state.view == View.Posts}
+ value={UserDetailsView.Posts}
+ checked={this.state.view == UserDetailsView.Posts}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('posts')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Saved && 'active'}
+ ${this.state.view == UserDetailsView.Saved && 'active'}
`}
>
<input
type="radio"
- value={View.Saved}
- checked={this.state.view == View.Saved}
+ value={UserDetailsView.Saved}
+ checked={this.state.view == UserDetailsView.Saved}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('saved')}
);
}
- overview() {
- let combined: Array<{ type_: string; data: Comment | Post }> = [];
- let comments = this.state.comments.map(e => {
- return { type_: 'comments', data: e };
- });
- let posts = this.state.posts.map(e => {
- return { type_: 'posts', data: e };
- });
-
- combined.push(...comments);
- combined.push(...posts);
-
- // Sort it
- if (this.state.sort == SortType.New) {
- combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
- } else {
- combined.sort((a, b) => b.data.score - a.data.score);
- }
-
- return (
- <div>
- {combined.map(i => (
- <div>
- {i.type_ == 'posts' ? (
- <PostListing
- post={i.data as Post}
- admins={this.state.admins}
- showCommunity
- enableDownvotes={this.state.site.enable_downvotes}
- enableNsfw={this.state.site.enable_nsfw}
- />
- ) : (
- <CommentNodes
- nodes={[{ comment: i.data as Comment }]}
- admins={this.state.admins}
- noIndent
- showContext
- enableDownvotes={this.state.site.enable_downvotes}
- />
- )}
- </div>
- ))}
- </div>
- );
- }
-
- comments() {
- return (
- <div>
- <CommentNodes
- nodes={commentsToFlatNodes(this.state.comments)}
- admins={this.state.admins}
- noIndent
- showContext
- enableDownvotes={this.state.site.enable_downvotes}
- />
- </div>
- );
- }
-
- posts() {
- return (
- <div>
- {this.state.posts.map(post => (
- <PostListing
- post={post}
- admins={this.state.admins}
- showCommunity
- enableDownvotes={this.state.site.enable_downvotes}
- enableNsfw={this.state.site.enable_nsfw}
- />
- ))}
- </div>
- );
- }
-
userInfo() {
let user = this.state.user;
return (
)}
</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>
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>
/>
</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
);
}
- paginator() {
- return (
- <div class="my-2">
- {this.state.page > 1 && (
- <button
- class="btn btn-sm btn-secondary mr-1"
- onClick={linkEvent(this, this.prevPage)}
- >
- {i18n.t('prev')}
- </button>
- )}
- <button
- class="btn btn-sm btn-secondary"
- onClick={linkEvent(this, this.nextPage)}
- >
- {i18n.t('next')}
- </button>
- </div>
- );
- }
-
- updateUrl() {
- let viewStr = View[this.state.view].toLowerCase();
- let sortStr = SortType[this.state.sort].toLowerCase();
+ updateUrl(paramUpdates: UrlParams) {
+ const page = paramUpdates.page || this.state.page;
+ const viewStr =
+ paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
+ const sortStr =
+ paramUpdates.sort || SortType[this.state.sort].toLowerCase();
this.props.history.push(
- `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
+ `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
);
}
- nextPage(i: User) {
- i.state.page++;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
- }
-
- prevPage(i: User) {
- i.state.page--;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
- }
-
- refetch() {
- let form: GetUserDetailsForm = {
- user_id: this.state.user_id,
- username: this.state.username,
- sort: SortType[this.state.sort],
- saved_only: this.state.view == View.Saved,
- page: this.state.page,
- limit: fetchLimit,
- };
- WebSocketService.Instance.getUserDetails(form);
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
}
handleSortChange(val: SortType) {
- this.state.sort = val;
- this.state.page = 1;
- this.setState(this.state);
- this.updateUrl();
- this.refetch();
+ this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
}
handleViewChange(i: User, event: any) {
- i.state.view = Number(event.target.value);
- i.state.page = 1;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
+ i.updateUrl({
+ view: UserDetailsView[Number(event.target.value)].toLowerCase(),
+ page: 1,
+ });
}
handleUserSettingsShowNsfwChange(i: User, event: any) {
});
}
+ 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;
}
parseMessage(msg: WebSocketJsonResponse) {
- console.log(msg);
- let res = wsJsonToRes(msg);
+ const res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
- this.state.deleteAccountLoading = false;
- this.state.avatarLoading = false;
- this.state.userSettingsLoading = false;
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
- this.setState(this.state);
+ this.setState({
+ deleteAccountLoading: false,
+ avatarLoading: false,
+ userSettingsLoading: false,
+ });
return;
- } else if (msg.reconnect) {
- this.refetch();
} else if (res.op == UserOperation.GetUserDetails) {
- let data = res.data as UserDetailsResponse;
- this.state.user = data.user;
- this.state.comments = data.comments;
- this.state.follows = data.follows;
- this.state.moderates = data.moderates;
- this.state.posts = data.posts;
- this.state.admins = data.admins;
- this.state.loading = false;
- if (this.isCurrentUser) {
- this.state.userSettingsForm.show_nsfw =
- UserService.Instance.user.show_nsfw;
- this.state.userSettingsForm.theme = UserService.Instance.user.theme
- ? UserService.Instance.user.theme
- : 'darkly';
- this.state.userSettingsForm.default_sort_type =
- UserService.Instance.user.default_sort_type;
- this.state.userSettingsForm.default_listing_type =
- UserService.Instance.user.default_listing_type;
- this.state.userSettingsForm.lang = UserService.Instance.user.lang;
- this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
- this.state.userSettingsForm.email = this.state.user.email;
- this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
- this.state.userSettingsForm.show_avatars =
- UserService.Instance.user.show_avatars;
- this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
- }
- document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
- window.scrollTo(0, 0);
- this.setState(this.state);
- setupTippy();
- } else if (res.op == UserOperation.EditComment) {
- let data = res.data as CommentResponse;
- editCommentRes(data, this.state.comments);
- this.setState(this.state);
- } else if (res.op == UserOperation.CreateComment) {
- let data = res.data as CommentResponse;
- if (
- UserService.Instance.user &&
- data.comment.creator_id == UserService.Instance.user.id
- ) {
- toast(i18n.t('reply_sent'));
+ // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
+ // and set the parent state if it is not set or differs
+ const data = res.data as UserDetailsResponse;
+
+ if (this.state.user.id !== data.user.id) {
+ this.state.user = data.user;
+ this.state.follows = data.follows;
+ this.state.moderates = data.moderates;
+
+ if (this.isCurrentUser) {
+ this.state.userSettingsForm.show_nsfw =
+ UserService.Instance.user.show_nsfw;
+ this.state.userSettingsForm.theme = UserService.Instance.user.theme
+ ? UserService.Instance.user.theme
+ : 'darkly';
+ this.state.userSettingsForm.default_sort_type =
+ UserService.Instance.user.default_sort_type;
+ this.state.userSettingsForm.default_listing_type =
+ UserService.Instance.user.default_listing_type;
+ this.state.userSettingsForm.lang = UserService.Instance.user.lang;
+ this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
+ this.state.userSettingsForm.email = this.state.user.email;
+ this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
+ this.state.userSettingsForm.show_avatars =
+ UserService.Instance.user.show_avatars;
+ this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
+ }
+ this.setState(this.state);
}
- } else if (res.op == UserOperation.SaveComment) {
- let data = res.data as CommentResponse;
- saveCommentRes(data, this.state.comments);
- this.setState(this.state);
- } else if (res.op == UserOperation.CreateCommentLike) {
- let data = res.data as CommentResponse;
- createCommentLikeRes(data, this.state.comments);
- this.setState(this.state);
- } else if (res.op == UserOperation.CreatePostLike) {
- let data = res.data as PostResponse;
- createPostLikeFindRes(data, this.state.posts);
- this.setState(this.state);
- } else if (res.op == UserOperation.BanUser) {
- let data = res.data as BanUserResponse;
- this.state.comments
- .filter(c => c.creator_id == data.user.id)
- .forEach(c => (c.banned = data.banned));
- this.state.posts
- .filter(c => c.creator_id == data.user.id)
- .forEach(c => (c.banned = data.banned));
- this.setState(this.state);
- } else if (res.op == UserOperation.AddAdmin) {
- let data = res.data as AddAdminResponse;
- this.state.admins = data.admins;
- 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);
+ const data = res.data as LoginResponse;
UserService.Instance.login(data);
+ this.setState({
+ userSettingsLoading: false,
+ });
+ window.scrollTo(0, 0);
} else if (res.op == UserOperation.DeleteAccount) {
- this.state.deleteAccountLoading = false;
- this.state.deleteAccountShowConfirm = false;
- this.setState(this.state);
+ this.setState({
+ deleteAccountLoading: false,
+ deleteAccountShowConfirm: false,
+ });
this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) {
- let data = res.data as GetSiteResponse;
- this.state.site = data.site;
- this.setState(this.state);
+ const data = res.data as GetSiteResponse;
+ this.setState({
+ site: data.site,
+ });
}
}
}
<!-- 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)" />
Old,
}
+export enum CommentViewType {
+ Tree,
+ Chat,
+}
+
export enum ListingType {
All,
Subscribed,
creator_actor_id: string;
creator_local: boolean;
creator_name: string;
+ creator_published: string;
creator_avatar?: string;
community_actor_id: string;
community_local: boolean;
local: boolean;
creator_id: number;
post_id: number;
+ post_name: string;
parent_id?: number;
content: string;
removed: boolean;
creator_local: boolean;
creator_name: string;
creator_avatar?: string;
+ creator_published: string;
score: number;
upvotes: number;
downvotes: number;
error?: string;
reconnect?: boolean;
}
+
+export enum UserDetailsView {
+ Overview,
+ Comments,
+ Posts,
+ Saved,
+}
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';
'litely',
];
-export const emojiPicker = new EmojiButton({
- // Use the emojiShortName from native
- style: 'twemoji',
- theme: 'dark',
- position: 'auto-start',
- // TODO i18n
-});
-
const DEFAULT_ALPHABET =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
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);
}
);
}
+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`;
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}:`;
// // 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);
+}
-export const version: string = 'v0.7.13';
+export const version: string = 'v0.7.21';
"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",
"Couldn't find that username or email.",
"password_incorrect": "Password incorrect.",
"passwords_dont_match": "Passwords do not match.",
+ "no_password_reset": "You will not be able to reset your password without an email.",
"invalid_username": "Invalid username.",
"admin_already_created": "Sorry, there's already an admin.",
"user_already_exists": "User already exists.",
"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"
}
{
- "post": "Argitaratu",
- "remove_post": "Argitalpena Ezabatu",
- "no_posts": "Argitalpenik gabe.",
- "create_a_post": "Argitalpen bat sortu",
- "number_of_posts": "Argitalpen {{count}}",
- "number_of_posts_plural": "{{count}} Argitalpen",
+ "post": "bidali",
+ "remove_post": "Ezabatu bidalketa",
+ "no_posts": "Ez dago bidalketarik.",
+ "create_a_post": "Sortu bidalketa bat",
+ "number_of_posts": "Bidalketa {{count}}",
+ "number_of_posts_plural": "{{count}} bidalketa",
"users": "Erabiltzaileak",
- "send_message": "Mezua Bidali",
- "message": "Mezu",
- "edit": "Editatu",
- "reply": "Erantzun",
+ "send_message": "Bidali mezua",
+ "message": "Mezua",
+ "edit": "editatu",
+ "reply": "erantzun",
"more": "gehiago",
- "upload_image": "Irudia igo",
+ "upload_image": "igo irudia",
"link": "esteka",
"remove": "ezabatu",
- "mark_as_unread": "irakurri gabe",
+ "mark_as_unread": "markatu ez irakurrita",
"delete": "ezabatu",
- "delete_account": "Ezabatu Kontua",
- "ban": "kaleratu",
- "ban_from_site": "kaleratu gunetik",
- "unban": "onartu",
+ "delete_account": "Ezabatu kontua",
+ "ban": "debekatu",
+ "ban_from_site": "debekatu gunean",
+ "unban": "kendu debekua",
"save": "gorde",
"create": "sortu",
"creator": "sortzailea",
- "username": "Erabiltzailearen izena",
+ "username": "Erabiltzaile-izena",
"name": "Izena",
"title": "Izenburua",
"both": "Biak",
"saved": "Gordeta",
- "week": "Astea",
- "month": "Hilabetea",
- "year": "Urtea",
- "all": "Dena",
+ "week": "Astekoak",
+ "month": "Hilabetekoak",
+ "year": "Urtekoak",
+ "all": "Guztiak",
"api": "API",
"unread": "Irakurri gabe",
"replies": "Erantzunak",
"search": "Bilatu",
- "sign_up": "Erregistratu",
+ "sign_up": "Eman izena",
"messages": "Mezuak",
"password": "Pasahitza",
- "password_change": "Pasahitza Aldatu",
- "new_password": "Pasahitz Berria",
- "email": "Posta elektronikoa",
+ "password_change": "Aldatu pasahitza",
+ "new_password": "Pasahitz berria",
+ "email": "Eposta",
"language": "Hizkuntza",
"url": "URL",
- "chat": "Txat",
+ "chat": "Txata",
"your_site": "zure gunea",
- "nsfw": "NSFW",
- "block_leaving": "Ziur zaude atera nahi duzula?",
+ "nsfw": "NSFW (eduki hunkigarria)",
+ "block_leaving": "Ziur al zaude atera nahi duzula?",
"bitcoin": "Bitcoin",
"ethereum": "Ethereum",
"monero": "Monero",
"yes": "bai",
"no": "ez",
- "couldnt_find_post": "Ezin izan da post-a aurkitu.",
- "couldnt_save_post": "Ezin izan da argitalpena gorde.",
- "site_already_exists": "Gunea jada existitzen da.",
+ "couldnt_find_post": "Ezin izan da bidalketarik aurkitu.",
+ "couldnt_save_post": "Ezin izan da bidalketa gorde.",
+ "site_already_exists": "Gunea dagoeneko existitzen da.",
"action": "Ekintza",
"time": "Denbora",
"number_of_points": "Puntu {{count}}",
- "number_of_points_plural": "{{count}} Puntu",
+ "number_of_points_plural": "{{count}} puntu",
"number_of_users": "Erabiltzaile {{count}}",
- "number_of_users_plural": "{{count}} Erabiltzaile",
- "number_of_subscribers": "Jarraitzaile {{count}}",
- "number_of_subscribers_plural": "{{count}} Jarraitzaile",
+ "number_of_users_plural": "{{count}} erabiltzaile",
+ "number_of_subscribers": "Harpidetu {{count}}",
+ "number_of_subscribers_plural": "{{count}} harpidetu",
"invalid_community_name": "Izen baliogabea.",
"click_to_delete_picture": "Egin klik irudia ezabatzeko.",
"picture_deleted": "Irudia ezabatuta.",
- "send_secure_message": "Mezu Segurua Bidali",
- "preview": "Aurretiko bista",
- "avatar": "Profilaren argazkia",
- "upload_avatar": "Profileko argazki bat igo",
- "show_avatars": "Profilen argazkia erakutsi",
+ "send_secure_message": "Bidali mezu segurua",
+ "preview": "Aurrebista",
+ "avatar": "Avatarra",
+ "upload_avatar": "Igo avatarra",
+ "show_avatars": "Erakutsi avatarrak",
"show_context": "Erakutsi testuingurua",
"formatting_help": "formatuaren laguntza",
- "sorting_help": "sailkatzeko laguntza",
- "view_source": "Iturria ikusi",
+ "sorting_help": "ordenatzeko laguntza",
+ "view_source": "Ikusi iturria",
"unlock": "desblokeatu",
"lock": "blokeatu",
"sticky": "finkatuta",
"unsticky": "finkatu gabe",
"archive_link": "artxiboko esteka",
- "mod": "Moderatzailea",
- "mods": "Moderatzaileak",
+ "mod": "moderatzailea",
+ "mods": "moderatzaileak",
"moderates": "Moderatuak",
- "settings": "Konfigurazioa",
- "admin_settings": "Administrazio-doikuntzak",
- "site_config": "Gunearen Konfigurazioa",
+ "settings": "Ezarpenak",
+ "admin_settings": "Administrazio ezarpenak",
+ "site_config": "Gunearen konfigurazioa",
"remove_as_mod": "ezabatu moderatzaile gisa",
- "modlog": "Moderatzailearen erregistroa",
- "appoint_as_mod": "moderatzaile bezala izendatu",
+ "modlog": "Moderazio loga",
+ "appoint_as_mod": "izendatu moderatzaile gisa",
"admin": "administratzailea",
"admins": "administratzaileak",
"remove_as_admin": "ezabatu administratzaile gisa",
- "appoint_as_admin": "administratzaile bezala izendatu",
+ "appoint_as_admin": "izendatu administratzaile gisa",
"removed": "moderatzaileak ezabatua",
"locked": "blokeatuta",
"number_online": "Erabiltzaile {{count}} konektatuta",
- "number_online_plural": "{{count}} Erabiltzaile konektatutak",
+ "number_online_plural": "{{count}} erabiltzaile konektatuta",
"subscribed": "Harpidetuta",
"prev": "Aurrekoa",
- "create_community": "Komunitate bat sortu",
- "create_post": "Sortu argitalpena",
- "posts": "Argitalpenak",
- "related_posts": "Argitalpen horiek zerikusia izan dezakete",
+ "create_community": "Sortu komunitatea",
+ "create_post": "Sortu bidalketa",
+ "posts": "Bidalketak",
+ "related_posts": "Bidalketa hauek zerikusia izan dezakete",
"cross_posts": "Esteka hau ere hemen argitaratu da:",
- "comments": "Iradokizunak",
- "number_of_comments": "Iradokizun {{count}}",
- "number_of_comments_plural": "{{count}} Iradokizun",
- "remove_comment": "Iradokizunak Ezabatu",
+ "comments": "Iruzkinak",
+ "number_of_comments": "Iruzkin {{count}}",
+ "number_of_comments_plural": "{{count}} iruzkin",
+ "remove_comment": "Ezabatu iruzkina",
"communities": "Komunitateak",
- "create_a_community": "Komunitate bat sortu",
- "cross_post": "Argitalpen gurutzatua",
- "cross_posted_to": "Argitalpen-gurutzatua: ",
+ "create_a_community": "Sortu komunitate bat",
+ "cross_post": "bidalketa gurutzatua",
+ "cross_posted_to": "bidalketa gurutzatua: ",
"next": "Hurrengoa",
- "remove_community": "Komunitatea ezabatu",
- "subscribed_to_communities": "<1>komunitateetan</1> harpidetuta",
- "trending_communities": "<1>komunitateen</1> joerak",
- "list_of_communities": "Komunitateen zerrenda",
- "community_reqs": "Letra xehez, azpimarratuta eta espaziorik gabe.",
- "create_private_message": "Mezu pribatu bat sortu",
+ "remove_community": "Ezabatu komunitatea",
+ "subscribed_to_communities": "<1>Komunitateetara</1> harpidetuta",
+ "trending_communities": "<1>Komunitateen</1> joerak",
+ "list_of_communities": "Komunitate-zerrenda",
+ "community_reqs": "Letra xehez, azpimarratuta eta hutsunerik gabe.",
+ "create_private_message": "Sortu mezu pribatua",
"cancel": "Ezeztatu",
"stickied": "finkatuta",
- "reason": "Arrazoi",
- "mark_as_read": "markatu irakurrita bezala",
+ "reason": "Arrazoia",
+ "mark_as_read": "markatu irakurrita gisa",
"deleted": "sortzaileak ezabatua",
- "delete_account_confirm": "Ohartarazpena: horrek etengabe ezabatuko ditu zure datu guztiak. Idatzi zure pasahitza baieztatzeko.",
+ "delete_account_confirm": "Abisua: honek zure datu guztiak betirako ezabatu ditu. Sartu zure pasahitza baieztatzeko.",
"restore": "leheneratu",
- "unban_from_site": "gunetik debekua kentzea",
- "banned": "kaleratuta",
- "banned_users": "Kaleratutako Erabiltzaileak",
+ "unban_from_site": "kendu debekua gunean",
+ "banned": "debekatuta",
+ "banned_users": "Debekatutako erabiltzaileak",
"unsave": "ez gorde",
- "email_or_username": "e-Posta edo Erabiltzailea",
+ "email_or_username": "Eposta edo erabiltzaile-izena",
"category": "Kategoria",
- "subscribers": "Jarraitzaileak",
- "unsubscribe": "Ez-harpidetu",
+ "subscribers": "Harpidetuak",
+ "unsubscribe": "Ezabatu harpidetza",
"subscribe": "Harpidetu",
"sidebar": "Alboko barra",
- "sort_type": "Sailkapen mota",
+ "sort_type": "Ordena-mota",
"hot": "Pil-pilean",
- "new": "Berri",
- "old": "Zahar",
- "top_day": "Eguneko hoberenak",
- "top": "Hoberena",
+ "new": "Berriak",
+ "old": "Zaharrak",
+ "top_day": "Gaur pil-pilean",
+ "top": "Bozkatuenak",
"docs": "Dokumentazioa",
- "inbox": "Mezuen sarrera",
- "inbox_for": "Sarrera-erretilua <1>{{user}}</1> -rentzat",
- "mark_all_as_read": "markatu dena irakurrita bezala",
+ "inbox": "Sarrera-erretilua",
+ "inbox_for": "<1>{{user}}</1>(r)en sarrera-erretilua",
+ "mark_all_as_read": "markatu guztiak irakurrita gisa",
"type": "Mota",
- "number_of_communities": "{{count}} Komunitate",
- "number_of_communities_plural": "{{count}} Komunitateak",
+ "number_of_communities": "Komunitate {{count}}",
+ "number_of_communities_plural": "{{count}} komunitate",
"mentions": "Aipamenak",
- "reply_sent": "Bidalitako erantzuna",
- "message_sent": "Bidalitako mezua",
- "overview": "Laburpen",
+ "reply_sent": "Erantzuna bidali da",
+ "message_sent": "Mezua bidali da",
+ "overview": "Laburpena",
"view": "Ikusi",
- "logout": "Irten",
- "login_sign_up": "Sartu / Kontua Sortu",
- "login": "Sartu",
- "notifications_error": "Mahaigaineko jakinarazpenak ez daude eskuragarri zure web-nabigatzailean. Probatu Firefox edo Chromerekin.",
+ "logout": "Itxi saioa",
+ "login_sign_up": "Hasi saioa / Eman izena",
+ "login": "Hasi saioa",
+ "notifications_error": "Mahaigaineko jakinarazpenak ez daude eskuragarri zure web-nabigatzailean. Probatu Firefoxekin edo Chromekin.",
"unread_messages": "Irakurri gabeko mezuak",
- "verify_password": "Pasahitza Balioztatu",
- "old_password": "Aurreko Pasahitza",
+ "verify_password": "Balioztatu pasahitza",
+ "old_password": "Aurreko pasahitza",
"forgot_password": "pasahitza ahaztu dut",
- "reset_password_mail_sent": "Mezu elektroniko bat bidali pasahitza berrezartzeko.",
- "no_email_setup": "Zerbitzari honek ez du posta elektronikoa behar bezala konfiguratu.",
- "matrix_user_id": "Matrix Erabiltzailea",
- "private_message_disclaimer": "Ohartarazpena: Lemmy-en dauden mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Riot.im</1> -en mezu seguruetarako.",
- "send_notifications_to_email": "Bidali jakinarazpenak posta elektronikora",
- "optional": "Ez-ohikoa",
- "browser_default": "Nabigatzaile Lehenetsia",
- "downvotes_disabled": "Puntuazio negatiboak desgaituta daude",
- "enable_downvotes": "Kontrako botoak gaitu",
- "upvote": "Aldeko botoa eman",
- "downvote": "Kontrako botoa eman",
- "number_of_downvotes": "Kontrako boto {{count}}",
- "number_of_downvotes_plural": "{{count}} Kontrako botoak",
- "number_of_upvotes": "Aldeko boto {{count}}",
- "number_of_upvotes_plural": "{{count}} Aldeko botoak",
- "open_registration": "Erregistro Irekia",
- "registration_closed": "Erregistroa itxita",
- "enable_nsfw": "NSFW gaitu",
+ "reset_password_mail_sent": "Eposta bat bidali da zure pasahitza berrezarri dezazun.",
+ "no_email_setup": "Zerbitzari honek ez du eposta ondo konfiguraturik.",
+ "matrix_user_id": "Matrix erabiltzailea",
+ "private_message_disclaimer": "Abisua: Lemmyko mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Riot.im</1>en mezu seguruak trukatzeko.",
+ "send_notifications_to_email": "Bidali jakinarazpenak epostara",
+ "optional": "Hautazkoa",
+ "browser_default": "Nabigatzaileko lehenetsia",
+ "downvotes_disabled": "Kontrako bozkak desgaituta",
+ "enable_downvotes": "Gaitu kontrako bozkak",
+ "upvote": "Alde bozkatu",
+ "downvote": "Kontra bozkatu",
+ "number_of_downvotes": "Kontrako bozka {{count}}",
+ "number_of_downvotes_plural": "{{count}} kontrako bozka",
+ "number_of_upvotes": "Aldeko bozka {{count}}",
+ "number_of_upvotes_plural": "{{count}} aldeko bozka",
+ "open_registration": "Izen-ematea irekia",
+ "registration_closed": "Izen-ematea itxira",
+ "enable_nsfw": "Gaitu NSFW (eduki hunkigarria)",
"body": "Gorputza",
- "copy_suggested_title": "Kopiatu iradokitako izenburua: {{title}}",
+ "copy_suggested_title": "kopiatu iradokitako izenburua: {{title}}",
"community": "Komunitatea",
"expand_here": "Hedatu hemen",
"subscribe_to_communities": "Harpidetu zaitez <1>komunitate</1> batzuetara.",
- "recent_comments": "Duela gutxiko Iruzkinak",
+ "recent_comments": "Iruzkin berrienak",
"select_a_community": "Aukeratu komunitate bat",
- "no_results": "Emaitzik gabe.",
- "setup": "Instalazioa",
- "lemmy_instance_setup": "Lemmy Instantziaren Konfigurazioa",
- "setup_admin": "Gunearen Administratzailea Konfiguratu",
+ "no_results": "Ez dago emaitzarik.",
+ "setup": "Ezarpenak",
+ "lemmy_instance_setup": "Lemmy instantziaren ezarpena",
+ "setup_admin": "Ezarri gunearen administratzailea",
"modified": "aldatuta",
- "show_nsfw": "Erakutsi NSFW edukia",
- "expires": "Iraungitzen",
- "theme": "Gaia",
+ "show_nsfw": "Erakutsi eduki hunkigarria (NSFW)",
+ "expires": "Noiz iraungitzen da:",
+ "theme": "Itxura",
"sponsors": "Babesleak",
- "sponsors_of_lemmy": "Lemmyren Babesleak",
- "sponsor_message": "Lemmy software librea da, <1>kode-irekia<1/>, publizitaterik, monetizaziorik edo arrisku kapitalik gabea, inoiz ez. Zuen dohainek zuzenean laguntzen dute proiektuaren lanaldi osoko garapena. Eskerrik asko honako pertsona hauei:",
- "support_on_patreon": "Patreon-en lagundu",
- "support_on_liberapay": "Liberpay-en lagundu",
- "support_on_open_collective": "OpenCollective-n lagundu",
- "donate_to_lemmy": "Dohaintza bat eman Lemmyri",
- "donate": "Dohaintza bat egin",
- "general_sponsors": "Lemmyri 10 eta 39 dolar artean emateko konpromisoa hartu zutenak dira Babesle Nagusiak.",
- "silver_sponsors": "Zilarrezko Babesleak dira Lemmyri 40 dolar eman zizkiotenak.",
- "crypto": "Kripto",
+ "sponsors_of_lemmy": "Lemmyren babesleak",
+ "sponsor_message": "Lemmy software librea da, <1>kode irekia</1>, publizitaterik, monetizaziorik eta arrisku kapitalik gabea, inoiz ez. Zuen dohaintzek zuzenean laguntzen dute proiektuaren lanaldi osoko garapena. Eskerrik asko honako pertsona hauei:",
+ "support_on_patreon": "Patreon bitartez lagundu",
+ "support_on_liberapay": "Liberpay bitartez lagundu",
+ "support_on_open_collective": "OpenCollective bitartez lagundu",
+ "donate_to_lemmy": "Egin dohaintza bat Lemmyri",
+ "donate": "Dohaintza egin",
+ "general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman zizkiotenak dira.",
+ "silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman zizkiotenak dira.",
+ "crypto": "Kriptomonetak",
"code": "Kodea",
"joined": "Batuta",
"by": "egilea",
- "to": "norentzako",
- "from": "hemendik",
+ "to": "nori",
+ "from": "nork",
"transfer_community": "transferentzia-komunitatea",
"transfer_site": "transferentzia-gunea",
- "are_you_sure": "Ziur ahal zaude?",
+ "are_you_sure": "ziur al zaude?",
"powered_by": "Egilea",
- "landing": "Lemmy <1>lotura-agregatzailea</1> /reddit alternatiboa da, eta <2>fedibertsoan</2> lan egiteko erabiltzen da. <3></3>Autohospedagarria da, iruzkin-hari eguneratuak ditu, eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide orrian dago. <5></5><6>Beta bertsio goiztiarra</6> da hau, eta ezaugarri asko hautsita edo desagertuta daude gaur egun. <7></7>Ezaugarri berriak iradokitzea edo akatsak jakinaraztea <8>hemen.</8><9></9> Rust, <11>Actix</11>, <12>Inferno</12>, <13>Typescriptekin egina</13>.",
- "logged_in": "Konektatuta zaude.",
- "not_logged_in": "Ez zaude konektatuta.",
- "site_saved": "Gunea Gordeta.",
+ "landing": "Lemmy <1>esteka-agregatzailea</1> / reddit-en ordezkoa da, eta <2>fedibertsoan</2> lan egiteko sortua da. <3></3>Norberak ostatu dezake, iruzkin-hari eguneratuak ditu eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide-orrian dago. <5></5>Hau <6>beta bertsio goiztiarra</6> da eta funtzionalitate asko hautsita edo egin gabe ditu oraindik. <7></7>Iradoki itzazu funtzionalitate berriak edo jakinarazi akatsak <8>hemen</8>.<9></9><10>Rust</10>, <11>Actix</11>, <12>Inferno</12> eta <13>Typescript</13>ekin egina.",
+ "logged_in": "Saioa hasi duzu.",
+ "not_logged_in": "Ez duzu saiorik hasi.",
+ "site_saved": "Gunea gorde da.",
"community_ban": "Komunitate honetan sartzea debekatu dizute.",
"site_ban": "Gune honetan sartzea debekatu dizute",
- "couldnt_create_comment": "Ezin izan da iruzkinik egin.",
- "couldnt_like_comment": "Ezin izan zaio iruzkin bati like bat eman.",
- "couldnt_update_comment": "Ezin izan zen iruzkina eguneratu.",
- "couldnt_save_comment": "Ezin izan zen iruzkina gorde.",
- "couldnt_get_comments": "Ezin izan ziren iruzkinak lortu.",
- "no_comment_edit_allowed": "Ezin da iruzkina editatu.",
- "no_post_edit_allowed": "Ezin da post-a editatu.",
- "no_community_edit_allowed": "Ezin da komunitatea editatu.",
- "couldnt_find_community": "Ezin izan da komunitatea aurkitu.",
- "couldnt_update_community": "Ezin izan zen komunitatea eguneratu.",
- "community_already_exists": "Komunitatea existitzen da jada.",
- "community_moderator_already_exists": "Komunitatearen moderatzailea existitzen da dagoeneko.",
- "community_follower_already_exists": "Jarraitzaileen komunitatea existitzen da dagoeneko.",
- "community_user_already_banned": "Komunitatearen erabiltzaile hau debekatuta dago jada.",
- "couldnt_create_post": "Argitalpena ezin izan da sortu.",
- "post_title_too_long": "Argitalpenaren izenburua luzeegia da.",
- "couldnt_like_post": "Ezin izan zaio like bat eman postari.",
- "couldnt_get_posts": "Ezin izan ziren postak lortu",
- "couldnt_update_post": "Ezin izan zen post-a eguneratu",
+ "couldnt_create_comment": "Ezin izan da iruzkina sortu.",
+ "couldnt_like_comment": "Ezin izan da iruzkinari datsegit eman.",
+ "couldnt_update_comment": "Ezin izan da iruzkina eguneratu.",
+ "couldnt_save_comment": "Ezin izan da iruzkina gorde.",
+ "couldnt_get_comments": "Ezin izan da iruzkinik lortu.",
+ "no_comment_edit_allowed": "Ezin duzu iruzkina editatu.",
+ "no_post_edit_allowed": "Ezin duzu bidalketa editatu.",
+ "no_community_edit_allowed": "Ezin duzu komunitatea editatu.",
+ "couldnt_find_community": "Ezin izan da komunitaterik aurkitu.",
+ "couldnt_update_community": "Ezin izan da komunitatea eguneratu.",
+ "community_already_exists": "Komunitate hori dagoeneko existitzen da.",
+ "community_moderator_already_exists": "Komunitateko moderatzaile hori dagoeneko existitzen da.",
+ "community_follower_already_exists": "Komunitateko jarraitzaile hori dagoeneko existitzen da.",
+ "community_user_already_banned": "Komunitateko erabiltzaile hau dagoeneko debekatuta dago.",
+ "couldnt_create_post": "Ezin izan da bidalketa sortu.",
+ "post_title_too_long": "Bidalketaren izenburua luzeegia da.",
+ "couldnt_like_post": "Ezin izan da bidalketari datsegit eman.",
+ "couldnt_get_posts": "Ezin izan da bidalketa lortu",
+ "couldnt_update_post": "Ezin izan da bidalketa eguneratu",
"no_slurs": "Irainik gabe.",
- "not_an_admin": "Ez da administratzailea.",
- "couldnt_update_site": "Ezinezkoa lekua berritzea.",
- "couldnt_find_that_username_or_email": "Ezin izan da aurkitu erabiltzaile-izen edo helbide elektroniko hori.",
- "password_incorrect": "Pasahitz desegokia.",
+ "not_an_admin": "Ez zara administratzailea.",
+ "couldnt_update_site": "Ezin izan da gunea eguneratu.",
+ "couldnt_find_that_username_or_email": "Ezin izan da aurkitu erabiltzaile-izen edo eposta hori.",
+ "password_incorrect": "Pasahitz okerra.",
"passwords_dont_match": "Pasahitzak ez dira berdinak.",
- "admin_already_created": "Barkatu, badago administratzaile bat dagoeneko.",
- "user_already_exists": "Erabiltzailea existitzen da dagoeneko.",
- "email_already_exists": "Posta helbide hau beste norbaitek erabiltzen du.",
- "couldnt_update_user": "Ezin izan zen erabiltzailea eguneratu.",
- "system_err_login": "Sistemaren errorea. Saiatu saioa ixten eta berriro sartzen.",
- "couldnt_create_private_message": "Ezinezkoa izan da mezu pribatua sortzea.",
- "no_private_message_edit_allowed": "Ezin da mezu pribaturik editatu.",
- "couldnt_update_private_message": "Ezinezkoa izan da mezu pribatua berritzea.",
- "emoji_picker": "Emoji Hautagailua",
- "invalid_username": "Erabiltzaile-izen baliogabea."
+ "admin_already_created": "Barkatu, dagoeneko badago administratzaile bat.",
+ "user_already_exists": "Erabiltzaile hori dagoeneko existitzen da.",
+ "email_already_exists": "Eposta hori dagoeneko existitzen da.",
+ "couldnt_update_user": "Ezin izan da erabiltzailea eguneratu.",
+ "system_err_login": "Sistemaren errorea. Saiatu saioa ixten eta berriz hasten.",
+ "couldnt_create_private_message": "Ezin izan da mezu pribatu hori sortu.",
+ "no_private_message_edit_allowed": "Ezin duzu mezu pribaturik editatu.",
+ "couldnt_update_private_message": "Ezin izan da mezu pribatu hori eguneratu.",
+ "emoji_picker": "Emoji hautagailua",
+ "invalid_username": "Erabiltzaile-izen baliogabea.",
+ "what_is": "Zer da"
}
"click_to_delete_picture": "Clicca per eliminare la foto.",
"picture_deleted": "Foto eliminata.",
"select_a_community": "Seleziona una comunità",
- "invalid_username": "Nome utente non valido."
+ "invalid_username": "Nome utente non valido.",
+ "what_is": "Cos'è"
}
-{}
+{
+ "password": "Lozinka",
+ "verify_password": "Potvrdite Loziku",
+ "old_password": "Stara Lozinka",
+ "reset_password_mail_sent": "Poslali smo Vam email za promenu lozinke.",
+ "private_message_disclaimer": "Napomena: Privatne poruke poslate putem Lemmy-a nisu osigurane. Molimo vas napravite nalog na <1>Riot.im</1> za bezbednu razmenu poruka.",
+ "browser_default": "Podrazumevano od strane pretraživača",
+ "number_of_upvotes_0": "{{count}} Gore-glas",
+ "number_of_upvotes_1": "{{count}} Gore-glasova",
+ "number_of_upvotes_2": "{{count}} Gore-glasa",
+ "subscribe_to_communities": "Pretplatite se nekim <1>zajednicama</1>.",
+ "show_nsfw": "Prikaži NSFW sadržaj",
+ "sponsor_message": "Lemmy je besplatan, <1>softver otvorenog koda</1>, bez reklamiranja, monetizovanja ili preduzetničkog kapitala, ikada. Vaše donacije direktno podržavaju aktivni razvoj projekta.Zahvaljujemo se sledećim ljudima:",
+ "donate_to_lemmy": "Donirajte Lemmy-u",
+ "general_sponsors": "Generalni Sponzori su oni koji su donirali između $10 i $39 Lemmy-u.",
+ "powered_by": "Sajt pokreće",
+ "forgot_password": "zaboravljena lozinka",
+ "password_change": "Promena Lozinke",
+ "new_password": "Nova Lozinka",
+ "no_email_setup": "Ovaj server nije pravilno namestio Vaš email.",
+ "email": "Email",
+ "matrix_user_id": "Korisnik Matrixa",
+ "send_notifications_to_email": "Primajte notifikacie na Vaš Email",
+ "optional": "Opcionalno",
+ "expires": "Ističe",
+ "language": "Jezik",
+ "downvotes_disabled": "Onemogućite negativne glasove",
+ "enable_downvotes": "Dozvolite negativne glasove",
+ "upvote": "Gore-glas",
+ "downvote": "Dole-glas",
+ "number_of_downvotes_0": "{{count}} Dole-glas",
+ "number_of_downvotes_1": "{{count}} Dole-glasova",
+ "number_of_downvotes_2": "{{count}} Dole-glasa",
+ "open_registration": "Otvorena Registracija",
+ "registration_closed": "Zatvorena registracija",
+ "enable_nsfw": "Dozvolite NSFW sadržaj",
+ "url": "URL",
+ "body": "Sadržaj",
+ "copy_suggested_title": "kopirajte predloženi naslov: {{title}}",
+ "community": "Zajednica",
+ "expand_here": "Proširite ovde",
+ "chat": "Ćaskanje",
+ "recent_comments": "Nedavni Komentari",
+ "no_results": "Nema rezultata.",
+ "setup": "Instalacioni proces",
+ "lemmy_instance_setup": "Instaliranje Lemmy Instance",
+ "setup_admin": "Napravite Administratorski Nalog",
+ "your_site": "Vaš sajt",
+ "modified": "izmenjeno",
+ "nsfw": "NSFW",
+ "theme": "Tema",
+ "sponsors": "Sponzori",
+ "sponsors_of_lemmy": "Sponzori Lemmy-a",
+ "support_on_patreon": "Podržite nas na Patreonu",
+ "support_on_liberapay": "Podržite nas na Liberpay-u",
+ "support_on_open_collective": "Podržite nas na OpenCollective",
+ "donate": "Donirajte",
+ "silver_sponsors": "Srebrni Sponzori su oni koji su donirali $40 Lemmy-u.",
+ "crypto": "Kripto",
+ "bitcoin": "Bitcoin",
+ "ethereum": "Ethereum",
+ "monero": "Monero",
+ "code": "Kod",
+ "joined": "Pridružio/la",
+ "by": "od",
+ "to": "do",
+ "from": "od",
+ "transfer_community": "transferujte zajednicu",
+ "transfer_site": "transferujte sajt",
+ "are_you_sure": "da li ste sigurni?",
+ "yes": "da",
+ "no": "ne"
+}
exec-sh "^0.3.2"
minimist "^1.2.0"
-"@fortawesome/fontawesome-common-types@^0.2.28":
- version "0.2.28"
- resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz#1091bdfe63b3f139441e9cba27aa022bff97d8b2"
- integrity sha512-gtis2/5yLdfI6n0ia0jH7NJs5i/Z/8M/ZbQL6jXQhCthEOe5Cr5NcQPhgTvFxNOtURE03/ZqUcEskdn2M+QaBg==
-
-"@fortawesome/fontawesome-svg-core@^1.2.22":
- version "1.2.28"
- resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.28.tgz#e5b8c8814ef375f01f5d7c132d3c3a2f83a3abf9"
- integrity sha512-4LeaNHWvrneoU0i8b5RTOJHKx7E+y7jYejplR7uSVB34+mp3Veg7cbKk7NBCLiI4TyoWS1wh9ZdoyLJR8wSAdg==
- dependencies:
- "@fortawesome/fontawesome-common-types" "^0.2.28"
-
-"@fortawesome/free-regular-svg-icons@^5.10.2":
- version "5.13.0"
- resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz#925a13d8bdda0678f71551828cac80ab47b8150c"
- integrity sha512-70FAyiS5j+ANYD4dh9NGowTorNDnyvQHHpCM7FpnF7GxtDjBUCKdrFqCPzesEIpNDFNd+La3vex+jDk4nnUfpA==
- dependencies:
- "@fortawesome/fontawesome-common-types" "^0.2.28"
-
-"@fortawesome/free-solid-svg-icons@^5.10.2":
- version "5.13.0"
- resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz#44d9118668ad96b4fd5c9434a43efc5903525739"
- integrity sha512-IHUgDJdomv6YtG4p3zl1B5wWf9ffinHIvebqQOmV3U+3SLw4fC+LUCCgwfETkbTtjy5/Qws2VoVf6z/ETQpFpg==
- dependencies:
- "@fortawesome/fontawesome-common-types" "^0.2.28"
-
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
-"@joeattardi/emoji-button@^2.12.1":
- version "2.12.1"
- resolved "https://registry.yarnpkg.com/@joeattardi/emoji-button/-/emoji-button-2.12.1.tgz#190df7c00721e04742ed6f8852db828798a4cf98"
- integrity sha512-rUuCXIcv4mRFK2IUKarYJN6J667wtH234smb1aQILzRf3/ycOoa6yUwnnvjxZeXMsPhuTnz15ndMOP2DhO5nNw==
- dependencies:
- "@fortawesome/fontawesome-svg-core" "^1.2.22"
- "@fortawesome/free-regular-svg-icons" "^5.10.2"
- "@fortawesome/free-solid-svg-icons" "^5.10.2"
- "@popperjs/core" "^2.0.0"
- focus-trap "^5.1.0"
- tiny-emitter "^2.1.0"
- tslib "^1.10.0"
- twemoji "^12.1.5"
-
-"@popperjs/core@^2.0.0":
- version "2.2.3"
- resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.2.3.tgz#0ae22b5650ab0b8fe508047245b66e71fc59e983"
- integrity sha512-68EQPzEZRrpFavFX40V2+80eqzQIhgza2AGTXW+i8laxSA4It+Y13rmZInrAYoIujp8YO7YJPbvgOesDZcIulQ==
-
"@popperjs/core@^2.2.0":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.3.2.tgz#1e56eb99bccddbda6a3e29aa4f3660f5b23edc43"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+choices.js@^9.0.1:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
+ integrity sha512-JgpeDY0Tmg7tqY6jaW/druSklJSt7W68tXFJIw0GSGWmO37SDAL8o60eICNGbzIODjj02VNNtf5h6TgoHDtCsA==
+ dependencies:
+ deepmerge "^4.2.0"
+ fuse.js "^3.4.5"
+ redux "^4.0.4"
+
chokidar@^1.6.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
-deepmerge@^4.2.2:
+deepmerge@^4.2.0, deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
dependencies:
chain-able "^1.0.1"
-focus-trap@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.1.0.tgz#64a0bfabd95c382103397dbc96bfef3a3cf8e5ad"
- integrity sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ==
- dependencies:
- tabbable "^4.0.0"
- xtend "^4.0.1"
-
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
jsonfile "^4.0.0"
universalify "^0.1.0"
-fs-extra@^8.0.1:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
- integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
- dependencies:
- graceful-fs "^4.2.0"
- jsonfile "^4.0.0"
- universalify "^0.1.0"
-
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
dependencies:
source-map "^0.6.1"
+fuse.js@^3.4.5:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
+ integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
+
gensync@^1.0.0-beta.1:
version "1.0.0-beta.1"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
dependencies:
type-fest "^0.8.1"
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
optionalDependencies:
graceful-fs "^4.1.6"
-jsonfile@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
- integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
- dependencies:
- universalify "^0.1.2"
- optionalDependencies:
- graceful-fs "^4.1.6"
-
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
dependencies:
minimist "0.0.8"
-mobius1-selectr@^2.4.13:
- version "2.4.13"
- resolved "https://registry.yarnpkg.com/mobius1-selectr/-/mobius1-selectr-2.4.13.tgz#0019dfd9f984840d6e40f70683ab3ec78ce3b5df"
- integrity sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw==
-
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
+redux@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+ integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+ dependencies:
+ loose-envify "^1.4.0"
+ symbol-observable "^1.2.0"
+
regenerate-unicode-properties@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
has-flag "^4.0.0"
supports-color "^7.0.0"
-symbol-observable@^1.1.0:
+symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
-tabbable@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
- integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
-
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
-tiny-emitter@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
- integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
-
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.3.tgz#2cc0eb125abdaff24b8298106a618ab7c6319edc"
integrity sha512-Pcg0PVQwJ7Fpv4+3R9obFNsrNKQyLbmUqsjeG7T7r4/4UTgIl0MSwurexjtuGpCp2iv2X/i9ffKPAfAOyYJ9og==
-tslib@^1.10.0:
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
- integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
-
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
-twemoji-parser@12.1.3:
- version "12.1.3"
- resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-12.1.3.tgz#916c0153e77bd5f1011e7a99cbeacf52e43c9371"
- integrity sha512-ND4LZXF4X92/PFrzSgGkq6KPPg8swy/U0yRw1k/+izWRVmq1HYi3khPwV3XIB6FRudgVICAaBhJfW8e8G3HC7Q==
-
-twemoji@^12.1.2, twemoji@^12.1.5:
- version "12.1.5"
- resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-12.1.5.tgz#a961fb65a1afcb1f729ad7e59391f9fe969820b9"
- integrity sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==
- dependencies:
- fs-extra "^8.0.1"
- jsonfile "^5.0.0"
- twemoji-parser "12.1.3"
- universalify "^0.1.2"
-
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
is-extendable "^0.1.1"
set-value "^2.0.1"
-universalify@^0.1.0, universalify@^0.1.2:
+universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
dependencies:
"@babel/runtime-corejs3" "^7.8.3"
-xtend@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
- integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"