Federation async (#848)

* Asyncify more

* I guess these changed

* Clean PR a bit

* Convert more away from failure error

* config changes for testing federation

* It was DNS

So actix-web's client relies on TRust DNS Resolver to figure out
where to send data, but TRust DNS Resolver seems to not play nice
with docker, which expressed itself as not resolving the name to
an IP address _the first time_ when making a request. The fix was
literally to make the request again (which I limited to 3 times
total, and not exceeding the request timeout in total)

* Only retry for connecterror

Since TRust DNS Resolver was causing ConnectError::Timeout,
this change limits the retry to only this error, returning
immediately for any other error

* Use http sig norm 0.4.0-alpha for actix-web 3.0 support

* Blocking function, retry http requests

* cargo +nightly fmt

* Only create one pictrs dir

* Don't yarn build

* cargo +nightly fmt
This commit is contained in:
Riley 2020-07-01 07:54:29 -05:00 committed by GitHub
parent 4c1cb5999c
commit a074564458
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 3846 additions and 2735 deletions

View file

@ -20,6 +20,8 @@ services:
postgres: postgres:
image: postgres:12-alpine image: postgres:12-alpine
ports:
- "127.0.0.1:5432:5432"
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password

View file

@ -5,17 +5,21 @@ pushd ../../server/
cargo build cargo build
popd popd
pushd ../../ui
yarn
popd
mkdir -p volumes/pictrs_{alpha,beta,gamma}
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
for Item in alpha beta gamma ; do sudo mkdir -p volumes/pictrs_alpha
sudo mkdir -p volumes/pictrs_$Item sudo chown -R 991:991 volumes/pictrs_alpha
sudo chown -R 991:991 volumes/pictrs_$Item
done
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d
pushd ../../ui pushd ../../ui
yarn
echo "Waiting for Lemmy to start..." echo "Waiting for Lemmy to start..."
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done

19
docker/federation-test/servers.sh vendored Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
set -e
sudo rm -rf volumes
pushd ../../server/
cargo build
popd
pushd ../../ui
yarn
popd
mkdir -p volumes/pictrs_{alpha,beta,gamma}
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up

10
docker/federation-test/tests.sh vendored Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
set -xe
pushd ../../ui
echo "Waiting for Lemmy to start..."
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
yarn api-test || true
popd

View file

@ -3,7 +3,7 @@ FROM ekidd/rust-musl-builder:1.42.0-openssl11
USER root USER root
RUN mkdir /app/dist/documentation/ -p \ RUN mkdir /app/dist/documentation/ -p \
&& addgroup --gid 1001 lemmy \ && addgroup --gid 1001 lemmy \
&& adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy && adduser --gecos "" --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
# Copy resources # Copy resources
COPY server/config/defaults.hjson /app/config/defaults.hjson COPY server/config/defaults.hjson /app/config/defaults.hjson

View file

@ -12,28 +12,33 @@ services:
- ../federation/nginx.conf:/etc/nginx/nginx.conf - ../federation/nginx.conf:/etc/nginx/nginx.conf
restart: on-failure restart: on-failure
depends_on: depends_on:
- lemmy_alpha - lemmy-alpha
- pictrs_alpha - pictrs
- lemmy_beta - lemmy-beta
- pictrs_beta - lemmy-gamma
- lemmy_gamma
- pictrs_gamma
- iframely - iframely
lemmy_alpha: pictrs:
restart: always
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_alpha:/mnt
lemmy-alpha:
image: lemmy-federation:latest image: lemmy-federation:latest
environment: environment:
- LEMMY_HOSTNAME=lemmy_alpha:8540 - LEMMY_HOSTNAME=lemmy-alpha:8540
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
- LEMMY_JWT_SECRET=changeme - LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_beta,lemmy_gamma - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma
- LEMMY_PORT=8540 - LEMMY_PORT=8540
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_alpha - LEMMY_SETUP__SITE_NAME=lemmy-alpha
- RUST_BACKTRACE=1 - RUST_BACKTRACE=1
- RUST_LOG=debug - RUST_LOG=debug
depends_on: depends_on:
@ -46,26 +51,21 @@ services:
- POSTGRES_DB=lemmy - POSTGRES_DB=lemmy
volumes: volumes:
- ./volumes/postgres_alpha:/var/lib/postgresql/data - ./volumes/postgres_alpha:/var/lib/postgresql/data
pictrs_alpha:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_alpha:/mnt
lemmy_beta: lemmy-beta:
image: lemmy-federation:latest image: lemmy-federation:latest
environment: environment:
- LEMMY_HOSTNAME=lemmy_beta:8550 - LEMMY_HOSTNAME=lemmy-beta:8550
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
- LEMMY_JWT_SECRET=changeme - LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_gamma - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma
- LEMMY_PORT=8550 - LEMMY_PORT=8550
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_beta - LEMMY_SETUP__SITE_NAME=lemmy-beta
- RUST_BACKTRACE=1 - RUST_BACKTRACE=1
- RUST_LOG=debug - RUST_LOG=debug
depends_on: depends_on:
@ -78,26 +78,21 @@ services:
- POSTGRES_DB=lemmy - POSTGRES_DB=lemmy
volumes: volumes:
- ./volumes/postgres_beta:/var/lib/postgresql/data - ./volumes/postgres_beta:/var/lib/postgresql/data
pictrs_beta:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_beta:/mnt
lemmy_gamma: lemmy-gamma:
image: lemmy-federation:latest image: lemmy-federation:latest
environment: environment:
- LEMMY_HOSTNAME=lemmy_gamma:8560 - LEMMY_HOSTNAME=lemmy-gamma:8560
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy
- LEMMY_JWT_SECRET=changeme - LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_beta - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta
- LEMMY_PORT=8560 - LEMMY_PORT=8560
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_gamma - LEMMY_SETUP__SITE_NAME=lemmy-gamma
- RUST_BACKTRACE=1 - RUST_BACKTRACE=1
- RUST_LOG=debug - RUST_LOG=debug
depends_on: depends_on:
@ -110,11 +105,6 @@ services:
- POSTGRES_DB=lemmy - POSTGRES_DB=lemmy
volumes: volumes:
- ./volumes/postgres_gamma:/var/lib/postgresql/data - ./volumes/postgres_gamma:/var/lib/postgresql/data
pictrs_gamma:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_gamma:/mnt
iframely: iframely:
image: dogbin/iframely:latest image: dogbin/iframely:latest

View file

@ -12,7 +12,7 @@ http {
client_max_body_size 50M; client_max_body_size 50M;
location / { location / {
proxy_pass http://lemmy_alpha:8540; proxy_pass http://lemmy-alpha:8540;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -26,7 +26,7 @@ http {
# pict-rs images # pict-rs images
location /pictrs { location /pictrs {
location /pictrs/image { location /pictrs/image {
proxy_pass http://pictrs_alpha:8080/image; proxy_pass http://pictrs:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -52,7 +52,7 @@ http {
client_max_body_size 50M; client_max_body_size 50M;
location / { location / {
proxy_pass http://lemmy_beta:8550; proxy_pass http://lemmy-beta:8550;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -66,7 +66,7 @@ http {
# pict-rs images # pict-rs images
location /pictrs { location /pictrs {
location /pictrs/image { location /pictrs/image {
proxy_pass http://pictrs_beta:8080/image; proxy_pass http://pictrs:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -92,7 +92,7 @@ http {
client_max_body_size 50M; client_max_body_size 50M;
location / { location / {
proxy_pass http://lemmy_gamma:8560; proxy_pass http://lemmy-gamma:8560;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -106,7 +106,7 @@ http {
# pict-rs images # pict-rs images
location /pictrs { location /pictrs {
location /pictrs/image { location /pictrs/image {
proxy_pass http://pictrs_gamma:8080/image; proxy_pass http://pictrs:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

569
server/Cargo.lock generated vendored

File diff suppressed because it is too large Load diff

15
server/Cargo.toml vendored
View file

@ -19,11 +19,12 @@ chrono = { version = "0.4.7", features = ["serde"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]} serde_json = { version = "1.0.52", features = ["preserve_order"]}
failure = "0.1.8" failure = "0.1.8"
serde = { version = "1.0.105", features = ["derive"] } serde = { version = "1.0.105", features = ["derive"] }
actix = "0.9.0" actix = "0.10.0-alpha.2"
actix-web = "2.0.0" actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] }
actix-files = "0.2.1" actix-files = "0.3.0-alpha.1"
actix-web-actors = "2.0.0" actix-web-actors = "3.0.0-alpha.1"
actix-rt = "1.1.1" actix-rt = "1.1.1"
awc = "2.0.0-alpha.2"
log = "0.4.0" log = "0.4.0"
env_logger = "0.7.1" env_logger = "0.7.1"
rand = "0.7.3" rand = "0.7.3"
@ -34,19 +35,19 @@ regex = "1.3.5"
lazy_static = "1.3.0" lazy_static = "1.3.0"
lettre = "0.9.3" lettre = "0.9.3"
lettre_email = "0.9.4" lettre_email = "0.9.4"
sha2 = "0.8.1"
rss = "1.9.0" rss = "1.9.0"
htmlescape = "0.3.1" htmlescape = "0.3.1"
url = { version = "2.1.1", features = ["serde"] } url = { version = "2.1.1", features = ["serde"] }
config = {version = "0.10.1", default-features = false, features = ["hjson"] } config = {version = "0.10.1", default-features = false, features = ["hjson"] }
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
isahc = "0.9.2"
comrak = "0.7" comrak = "0.7"
openssl = "0.10" openssl = "0.10"
http = "0.2.1" http = "0.2.1"
http-signature-normalization = "0.5.1" http-signature-normalization-actix = { version = "0.4.0-alpha.0", default-features = false, features = ["sha-2"] }
base64 = "0.12.1" base64 = "0.12.1"
tokio = "0.2.21" tokio = "0.2.21"
futures = "0.3.5" futures = "0.3.5"
itertools = "0.9.0" itertools = "0.9.0"
uuid = { version = "0.8", features = ["serde", "v4"] } uuid = { version = "0.8", features = ["serde", "v4"] }
sha2 = "0.9"
async-trait = "0.1.36"

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking,
db::{ db::{
comment::*, comment::*,
comment_view::*, comment_view::*,
@ -27,13 +28,10 @@ use crate::{
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool,
LemmyError,
MentionData, MentionData,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -97,14 +95,15 @@ pub struct GetCommentsResponse {
comments: Vec<CommentView>, comments: Vec<CommentView>,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateComment> { impl Perform for Oper<CreateComment> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &CreateComment = &self.data; let data: &CreateComment = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -114,20 +113,6 @@ impl Perform for Oper<CreateComment> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
let content_slurs_removed = remove_slurs(&data.content.to_owned()); let content_slurs_removed = remove_slurs(&data.content.to_owned());
let comment_form = CommentForm { let comment_form = CommentForm {
@ -144,21 +129,48 @@ impl Perform for Oper<CreateComment> {
local: true, local: true,
}; };
let inserted_comment = match Comment::create(&conn, &comment_form) { // Check for a community ban
let post_id = data.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
let comment_form2 = comment_form.clone();
let inserted_comment =
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| {
Comment::update_ap_id(&conn, inserted_comment_id)
})
.await?
{
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
}; };
let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.id) { updated_comment
Ok(comment) => comment, .send_create(&user, &self.client, pool)
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), .await?;
};
updated_comment.send_create(&user, &conn)?;
// Scan the comment for user mentions, add those rows // Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&comment_form.content); let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post); let recipient_ids =
send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?;
// You like your own comment by default // You like your own comment by default
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
@ -168,14 +180,17 @@ impl Perform for Oper<CreateComment> {
score: 1, score: 1,
}; };
let _inserted_like = match CommentLike::like(&conn, &like_form) { let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
Ok(like) => like, if blocking(pool, like).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), return Err(APIError::err("couldnt_like_comment").into());
}; }
updated_comment.send_like(&user, &conn)?; updated_comment.send_like(&user, &self.client, pool).await?;
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?; let comment_view = blocking(pool, move |conn| {
CommentView::read(&conn, inserted_comment.id, Some(user_id))
})
.await??;
let mut res = CommentResponse { let mut res = CommentResponse {
comment: comment_view, comment: comment_view,
@ -198,14 +213,15 @@ impl Perform for Oper<CreateComment> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditComment> { impl Perform for Oper<EditComment> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &EditComment = &self.data; let data: &EditComment = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -215,30 +231,44 @@ impl Perform for Oper<EditComment> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
let user = User_::read(&conn, user_id)?; let edit_id = data.edit_id;
let orig_comment =
let orig_comment = CommentView::read(&conn, data.edit_id, None)?; blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// You are allowed to mark the comment as read even if you're banned. // You are allowed to mark the comment as read even if you're banned.
if data.read.is_none() { if data.read.is_none() {
// Verify its the creator or a mod, or an admin // Verify its the creator or a mod, or an admin
let mut editors: Vec<i32> = vec![data.creator_id]; let mut editors: Vec<i32> = vec![data.creator_id];
let community_id = orig_comment.community_id;
editors.append( editors.append(
&mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)? &mut blocking(pool, move |conn| {
.into_iter() Ok(
.map(|m| m.user_id) CommunityModeratorView::for_community(&conn, community_id)?
.collect(), .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??,
); );
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err("no_comment_edit_allowed").into()); return Err(APIError::err("no_comment_edit_allowed").into());
} }
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() { let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
@ -250,7 +280,8 @@ impl Perform for Oper<EditComment> {
let content_slurs_removed = remove_slurs(&data.content.to_owned()); let content_slurs_removed = remove_slurs(&data.content.to_owned());
let read_comment = Comment::read(&conn, data.edit_id)?; let edit_id = data.edit_id;
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
let comment_form = CommentForm { let comment_form = CommentForm {
content: content_slurs_removed, content: content_slurs_removed,
@ -270,31 +301,48 @@ impl Perform for Oper<EditComment> {
local: read_comment.local, local: read_comment.local,
}; };
let updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) { let edit_id = data.edit_id;
let comment_form2 = comment_form.clone();
let updated_comment = match blocking(pool, move |conn| {
Comment::update(conn, edit_id, &comment_form2)
})
.await?
{
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
}; };
if let Some(deleted) = data.deleted.to_owned() { if let Some(deleted) = data.deleted.to_owned() {
if deleted { if deleted {
updated_comment.send_delete(&user, &conn)?; updated_comment
.send_delete(&user, &self.client, pool)
.await?;
} else { } else {
updated_comment.send_undo_delete(&user, &conn)?; updated_comment
.send_undo_delete(&user, &self.client, pool)
.await?;
} }
} else if let Some(removed) = data.removed.to_owned() { } else if let Some(removed) = data.removed.to_owned() {
if removed { if removed {
updated_comment.send_remove(&user, &conn)?; updated_comment
.send_remove(&user, &self.client, pool)
.await?;
} else { } else {
updated_comment.send_undo_remove(&user, &conn)?; updated_comment
.send_undo_remove(&user, &self.client, pool)
.await?;
} }
} else { } else {
updated_comment.send_update(&user, &conn)?; updated_comment
.send_update(&user, &self.client, pool)
.await?;
} }
let post = Post::read(&conn, data.post_id)?; let post_id = data.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&comment_form.content); let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post); let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
// Mod tables // Mod tables
if let Some(removed) = data.removed.to_owned() { if let Some(removed) = data.removed.to_owned() {
@ -304,10 +352,14 @@ impl Perform for Oper<EditComment> {
removed: Some(removed), removed: Some(removed),
reason: data.reason.to_owned(), reason: data.reason.to_owned(),
}; };
ModRemoveComment::create(&conn, &form)?; blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
} }
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?; let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
let mut res = CommentResponse { let mut res = CommentResponse {
comment: comment_view, comment: comment_view,
@ -330,14 +382,15 @@ impl Perform for Oper<EditComment> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SaveComment> { impl Perform for Oper<SaveComment> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &SaveComment = &self.data; let data: &SaveComment = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -352,21 +405,23 @@ impl Perform for Oper<SaveComment> {
user_id, user_id,
}; };
let conn = pool.get()?;
if data.save { if data.save {
match CommentSaved::save(&conn, &comment_saved_form) { let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
Ok(comment) => comment, if blocking(pool, save_comment).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), return Err(APIError::err("couldnt_save_comment").into());
}; }
} else { } else {
match CommentSaved::unsave(&conn, &comment_saved_form) { let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
Ok(comment) => comment, if blocking(pool, unsave_comment).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), return Err(APIError::err("couldnt_save_comment").into());
}; }
} }
let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?; let comment_id = data.comment_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
})
.await??;
Ok(CommentResponse { Ok(CommentResponse {
comment: comment_view, comment: comment_view,
@ -375,14 +430,15 @@ impl Perform for Oper<SaveComment> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateCommentLike> { impl Perform for Oper<CreateCommentLike> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &CreateCommentLike = &self.data; let data: &CreateCommentLike = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -394,36 +450,42 @@ impl Perform for Oper<CreateCommentLike> {
let mut recipient_ids = Vec::new(); let mut recipient_ids = Vec::new();
let conn = pool.get()?;
// Don't do a downvote if site has downvotes disabled // Don't do a downvote if site has downvotes disabled
if data.score == -1 { if data.score == -1 {
let site = SiteView::read(&conn)?; let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
if !site.enable_downvotes { if !site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into()); return Err(APIError::err("downvotes_disabled").into());
} }
} }
// Check for a community ban // Check for a community ban
let post = Post::read(&conn, data.post_id)?; let post_id = data.post_id;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
let comment = Comment::read(&conn, data.comment_id)?; let comment_id = data.comment_id;
let comment = blocking(pool, move |conn| Comment::read(conn, comment_id)).await??;
// Add to recipient ids // Add to recipient ids
match comment.parent_id { match comment.parent_id {
Some(parent_id) => { Some(parent_id) => {
let parent_comment = Comment::read(&conn, parent_id)?; let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
if parent_comment.creator_id != user_id { if parent_comment.creator_id != user_id {
let parent_user = User_::read(&conn, parent_comment.creator_id)?; let parent_user = blocking(pool, move |conn| {
User_::read(conn, parent_comment.creator_id)
})
.await??;
recipient_ids.push(parent_user.id); recipient_ids.push(parent_user.id);
} }
} }
@ -440,27 +502,33 @@ impl Perform for Oper<CreateCommentLike> {
}; };
// Remove any likes first // Remove any likes first
CommentLike::remove(&conn, &like_form)?; let like_form2 = like_form.clone();
blocking(pool, move |conn| CommentLike::remove(conn, &like_form2)).await??;
// Only add the like if the score isnt 0 // Only add the like if the score isnt 0
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add { if do_add {
let _inserted_like = match CommentLike::like(&conn, &like_form) { let like_form2 = like_form.clone();
Ok(like) => like, let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), if blocking(pool, like).await?.is_err() {
}; return Err(APIError::err("couldnt_like_comment").into());
}
if like_form.score == 1 { if like_form.score == 1 {
comment.send_like(&user, &conn)?; comment.send_like(&user, &self.client, pool).await?;
} else if like_form.score == -1 { } else if like_form.score == -1 {
comment.send_dislike(&user, &conn)?; comment.send_dislike(&user, &self.client, pool).await?;
} }
} else { } else {
comment.send_undo_like(&user, &conn)?; comment.send_undo_like(&user, &self.client, pool).await?;
} }
// Have to refetch the comment to get the current state // Have to refetch the comment to get the current state
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?; let comment_id = data.comment_id;
let liked_comment = blocking(pool, move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
})
.await??;
let mut res = CommentResponse { let mut res = CommentResponse {
comment: liked_comment, comment: liked_comment,
@ -483,14 +551,15 @@ impl Perform for Oper<CreateCommentLike> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetComments> { impl Perform for Oper<GetComments> {
type Response = GetCommentsResponse; type Response = GetCommentsResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetCommentsResponse, Error> { ) -> Result<GetCommentsResponse, LemmyError> {
let data: &GetComments = &self.data; let data: &GetComments = &self.data;
let user_claims: Option<Claims> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
@ -509,19 +578,23 @@ impl Perform for Oper<GetComments> {
let type_ = ListingType::from_str(&data.type_)?; let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let conn = pool.get()?; let community_id = data.community_id;
let page = data.page;
let comments = match CommentQueryBuilder::create(&conn) let limit = data.limit;
.listing_type(type_) let comments = blocking(pool, move |conn| {
.sort(&sort) CommentQueryBuilder::create(conn)
.for_community_id(data.community_id) .listing_type(type_)
.my_user_id(user_id) .sort(&sort)
.page(data.page) .for_community_id(community_id)
.limit(data.limit) .my_user_id(user_id)
.list() .page(page)
{ .limit(limit)
.list()
})
.await?;
let comments = match comments {
Ok(comments) => comments, Ok(comments) => comments,
Err(_e) => return Err(APIError::err("couldnt_get_comments").into()), Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
}; };
if let Some(ws) = websocket_info { if let Some(ws) = websocket_info {
@ -542,8 +615,23 @@ impl Perform for Oper<GetComments> {
} }
} }
pub fn send_local_notifs( pub async fn send_local_notifs(
conn: &PgConnection, mentions: Vec<MentionData>,
comment: Comment,
user: User_,
post: Post,
pool: &DbPool,
) -> Result<Vec<i32>, LemmyError> {
let ids = blocking(pool, move |conn| {
do_send_local_notifs(conn, &mentions, &comment, &user, &post)
})
.await?;
Ok(ids)
}
fn do_send_local_notifs(
conn: &diesel::PgConnection,
mentions: &[MentionData], mentions: &[MentionData],
comment: &Comment, comment: &Comment,
user: &User_, user: &User_,

View file

@ -7,6 +7,7 @@ use crate::{
ActorType, ActorType,
EndpointType, EndpointType,
}, },
blocking,
db::{Bannable, Crud, Followable, Joinable, SortType}, db::{Bannable, Crud, Followable, Joinable, SortType},
is_valid_community_name, is_valid_community_name,
naive_from_unix, naive_from_unix,
@ -18,12 +19,9 @@ use crate::{
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool,
LemmyError,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -138,14 +136,15 @@ pub struct TransferCommunity {
auth: String, auth: String,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetCommunity> { impl Perform for Oper<GetCommunity> {
type Response = GetCommunityResponse; type Response = GetCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetCommunityResponse, Error> { ) -> Result<GetCommunityResponse, LemmyError> {
let data: &GetCommunity = &self.data; let data: &GetCommunity = &self.data;
let user_id: Option<i32> = match &data.auth { let user_id: Option<i32> = match &data.auth {
@ -159,33 +158,38 @@ impl Perform for Oper<GetCommunity> {
None => None, None => None,
}; };
let conn = pool.get()?; let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
let community = match data.id { let community = match data.id {
Some(id) => Community::read(&conn, id)?, Some(id) => blocking(pool, move |conn| Community::read(conn, id)).await??,
None => { None => match blocking(pool, move |conn| Community::read_from_name(conn, &name)).await? {
match Community::read_from_name( Ok(community) => community,
&conn, Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
&data.name.to_owned().unwrap_or_else(|| "main".to_string()), },
) {
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}
}
}; };
let community_view = match CommunityView::read(&conn, community.id, user_id) { let community_id = community.id;
let community_view = match blocking(pool, move |conn| {
CommunityView::read(conn, community_id, user_id)
})
.await?
{
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let moderators = match CommunityModeratorView::for_community(&conn, community.id) { let community_id = community.id;
let moderators: Vec<CommunityModeratorView> = match blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators, Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let site_creator_id = Site::read(&conn, 1)?.creator_id; let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let mut admins = UserView::admins(&conn)?; let site_creator_id = site.creator_id;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
@ -220,14 +224,15 @@ impl Perform for Oper<GetCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateCommunity> { impl Perform for Oper<CreateCommunity> {
type Response = CommunityResponse; type Response = CommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, Error> { ) -> Result<CommunityResponse, LemmyError> {
let data: &CreateCommunity = &self.data; let data: &CreateCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -255,10 +260,9 @@ impl Perform for Oper<CreateCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if user_view.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
@ -283,34 +287,36 @@ impl Perform for Oper<CreateCommunity> {
published: None, published: None,
}; };
let inserted_community = match Community::create(&conn, &community_form) { let inserted_community =
Ok(community) => community, match blocking(pool, move |conn| Community::create(conn, &community_form)).await? {
Err(_e) => return Err(APIError::err("community_already_exists").into()), Ok(community) => community,
}; Err(_e) => return Err(APIError::err("community_already_exists").into()),
};
let community_moderator_form = CommunityModeratorForm { let community_moderator_form = CommunityModeratorForm {
community_id: inserted_community.id, community_id: inserted_community.id,
user_id, user_id,
}; };
let _inserted_community_moderator = let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
match CommunityModerator::join(&conn, &community_moderator_form) { if blocking(pool, join).await?.is_err() {
Ok(user) => user, return Err(APIError::err("community_moderator_already_exists").into());
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }
};
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id, community_id: inserted_community.id,
user_id, user_id,
}; };
let _inserted_community_follower = let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
match CommunityFollower::follow(&conn, &community_follower_form) { if blocking(pool, follow).await?.is_err() {
Ok(user) => user, return Err(APIError::err("community_follower_already_exists").into());
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), }
};
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?; let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, inserted_community.id, Some(user_id))
})
.await??;
Ok(CommunityResponse { Ok(CommunityResponse {
community: community_view, community: community_view,
@ -318,14 +324,15 @@ impl Perform for Oper<CreateCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditCommunity> { impl Perform for Oper<EditCommunity> {
type Response = CommunityResponse; type Response = CommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, Error> { ) -> Result<CommunityResponse, LemmyError> {
let data: &EditCommunity = &self.data; let data: &EditCommunity = &self.data;
if let Err(slurs) = slur_check(&data.name) { if let Err(slurs) = slur_check(&data.name) {
@ -353,28 +360,34 @@ impl Perform for Oper<EditCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Verify its a mod // Verify its a mod
let edit_id = data.edit_id;
let mut editors: Vec<i32> = Vec::new(); let mut editors: Vec<i32> = Vec::new();
editors.append( editors.append(
&mut CommunityModeratorView::for_community(&conn, data.edit_id)? &mut blocking(pool, move |conn| {
.into_iter() CommunityModeratorView::for_community(conn, edit_id)
.map(|m| m.user_id) .map(|v| v.into_iter().map(|m| m.user_id).collect())
.collect(), })
.await??,
);
editors.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
); );
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err("no_community_edit_allowed").into()); return Err(APIError::err("no_community_edit_allowed").into());
} }
let read_community = Community::read(&conn, data.edit_id)?; let edit_id = data.edit_id;
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
let community_form = CommunityForm { let community_form = CommunityForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -394,7 +407,12 @@ impl Perform for Oper<EditCommunity> {
published: None, published: None,
}; };
let updated_community = match Community::update(&conn, data.edit_id, &community_form) { let edit_id = data.edit_id;
let updated_community = match blocking(pool, move |conn| {
Community::update(conn, edit_id, &community_form)
})
.await?
{
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()), Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
}; };
@ -412,24 +430,36 @@ impl Perform for Oper<EditCommunity> {
reason: data.reason.to_owned(), reason: data.reason.to_owned(),
expires, expires,
}; };
ModRemoveCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
} }
if let Some(deleted) = data.deleted.to_owned() { if let Some(deleted) = data.deleted.to_owned() {
if deleted { if deleted {
updated_community.send_delete(&user, &conn)?; updated_community
.send_delete(&user, &self.client, pool)
.await?;
} else { } else {
updated_community.send_undo_delete(&user, &conn)?; updated_community
.send_undo_delete(&user, &self.client, pool)
.await?;
} }
} else if let Some(removed) = data.removed.to_owned() { } else if let Some(removed) = data.removed.to_owned() {
if removed { if removed {
updated_community.send_remove(&user, &conn)?; updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else { } else {
updated_community.send_undo_remove(&user, &conn)?; updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
} }
} }
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?; let edit_id = data.edit_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommunityResponse { let res = CommunityResponse {
community: community_view, community: community_view,
@ -453,14 +483,15 @@ impl Perform for Oper<EditCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<ListCommunities> { impl Perform for Oper<ListCommunities> {
type Response = ListCommunitiesResponse; type Response = ListCommunitiesResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<ListCommunitiesResponse, Error> { ) -> Result<ListCommunitiesResponse, LemmyError> {
let data: &ListCommunities = &self.data; let data: &ListCommunities = &self.data;
let user_claims: Option<Claims> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
@ -483,29 +514,33 @@ impl Perform for Oper<ListCommunities> {
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let conn = pool.get()?; let page = data.page;
let limit = data.limit;
let communities = CommunityQueryBuilder::create(&conn) let communities = blocking(pool, move |conn| {
.sort(&sort) CommunityQueryBuilder::create(conn)
.for_user(user_id) .sort(&sort)
.show_nsfw(show_nsfw) .for_user(user_id)
.page(data.page) .show_nsfw(show_nsfw)
.limit(data.limit) .page(page)
.list()?; .limit(limit)
.list()
})
.await??;
// Return the jwt // Return the jwt
Ok(ListCommunitiesResponse { communities }) Ok(ListCommunitiesResponse { communities })
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<FollowCommunity> { impl Perform for Oper<FollowCommunity> {
type Response = CommunityResponse; type Response = CommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, Error> { ) -> Result<CommunityResponse, LemmyError> {
let data: &FollowCommunity = &self.data; let data: &FollowCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -515,9 +550,8 @@ impl Perform for Oper<FollowCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let community_id = data.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let community = Community::read(&conn, data.community_id)?;
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: data.community_id, community_id: data.community_id,
user_id, user_id,
@ -525,34 +559,44 @@ impl Perform for Oper<FollowCommunity> {
if community.local { if community.local {
if data.follow { if data.follow {
match CommunityFollower::follow(&conn, &community_follower_form) { let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
Ok(user) => user, if blocking(pool, follow).await?.is_err() {
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), return Err(APIError::err("community_follower_already_exists").into());
}; }
} else { } else {
match CommunityFollower::unfollow(&conn, &community_follower_form) { let unfollow =
Ok(user) => user, move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), if blocking(pool, unfollow).await?.is_err() {
}; return Err(APIError::err("community_follower_already_exists").into());
}
} }
} else { } else {
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if data.follow { if data.follow {
// Dont actually add to the community followers here, because you need // Dont actually add to the community followers here, because you need
// to wait for the accept // to wait for the accept
user.send_follow(&community.actor_id, &conn)?; user
.send_follow(&community.actor_id, &self.client, pool)
.await?;
} else { } else {
user.send_unfollow(&community.actor_id, &conn)?; user
match CommunityFollower::unfollow(&conn, &community_follower_form) { .send_unfollow(&community.actor_id, &self.client, pool)
Ok(user) => user, .await?;
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), let unfollow =
}; move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(pool, unfollow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
}
} }
// TODO: this needs to return a "pending" state, until Accept is received from the remote server // TODO: this needs to return a "pending" state, until Accept is received from the remote server
} }
let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?; let community_id = data.community_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, community_id, Some(user_id))
})
.await??;
Ok(CommunityResponse { Ok(CommunityResponse {
community: community_view, community: community_view,
@ -560,14 +604,15 @@ impl Perform for Oper<FollowCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetFollowedCommunities> { impl Perform for Oper<GetFollowedCommunities> {
type Response = GetFollowedCommunitiesResponse; type Response = GetFollowedCommunitiesResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetFollowedCommunitiesResponse, Error> { ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
let data: &GetFollowedCommunities = &self.data; let data: &GetFollowedCommunities = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -577,27 +622,29 @@ impl Perform for Oper<GetFollowedCommunities> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let communities = match blocking(pool, move |conn| {
CommunityFollowerView::for_user(conn, user_id)
let communities: Vec<CommunityFollowerView> = })
match CommunityFollowerView::for_user(&conn, user_id) { .await?
Ok(communities) => communities, {
Err(_e) => return Err(APIError::err("system_err_login").into()), Ok(communities) => communities,
}; _ => return Err(APIError::err("system_err_login").into()),
};
// Return the jwt // Return the jwt
Ok(GetFollowedCommunitiesResponse { communities }) Ok(GetFollowedCommunitiesResponse { communities })
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<BanFromCommunity> { impl Perform for Oper<BanFromCommunity> {
type Response = BanFromCommunityResponse; type Response = BanFromCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<BanFromCommunityResponse, Error> { ) -> Result<BanFromCommunityResponse, LemmyError> {
let data: &BanFromCommunity = &self.data; let data: &BanFromCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -612,18 +659,16 @@ impl Perform for Oper<BanFromCommunity> {
user_id: data.user_id, user_id: data.user_id,
}; };
let conn = pool.get()?;
if data.ban { if data.ban {
match CommunityUserBan::ban(&conn, &community_user_ban_form) { let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
Ok(user) => user, if blocking(pool, ban).await?.is_err() {
Err(_e) => return Err(APIError::err("community_user_already_banned").into()), return Err(APIError::err("community_user_already_banned").into());
}; }
} else { } else {
match CommunityUserBan::unban(&conn, &community_user_ban_form) { let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
Ok(user) => user, if blocking(pool, unban).await?.is_err() {
Err(_e) => return Err(APIError::err("community_user_already_banned").into()), return Err(APIError::err("community_user_already_banned").into());
}; }
} }
// Mod tables // Mod tables
@ -640,9 +685,10 @@ impl Perform for Oper<BanFromCommunity> {
banned: Some(data.ban), banned: Some(data.ban),
expires, expires,
}; };
ModBanFromCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModBanFromCommunity::create(conn, &form)).await??;
let user_view = UserView::read(&conn, data.user_id)?; let user_id = data.user_id;
let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
let res = BanFromCommunityResponse { let res = BanFromCommunityResponse {
user: user_view, user: user_view,
@ -662,14 +708,15 @@ impl Perform for Oper<BanFromCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<AddModToCommunity> { impl Perform for Oper<AddModToCommunity> {
type Response = AddModToCommunityResponse; type Response = AddModToCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<AddModToCommunityResponse, Error> { ) -> Result<AddModToCommunityResponse, LemmyError> {
let data: &AddModToCommunity = &self.data; let data: &AddModToCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -684,18 +731,16 @@ impl Perform for Oper<AddModToCommunity> {
user_id: data.user_id, user_id: data.user_id,
}; };
let conn = pool.get()?;
if data.added { if data.added {
match CommunityModerator::join(&conn, &community_moderator_form) { let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
Ok(user) => user, if blocking(pool, join).await?.is_err() {
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), return Err(APIError::err("community_moderator_already_exists").into());
}; }
} else { } else {
match CommunityModerator::leave(&conn, &community_moderator_form) { let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
Ok(user) => user, if blocking(pool, leave).await?.is_err() {
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), return Err(APIError::err("community_moderator_already_exists").into());
}; }
} }
// Mod tables // Mod tables
@ -705,9 +750,13 @@ impl Perform for Oper<AddModToCommunity> {
community_id: data.community_id, community_id: data.community_id,
removed: Some(!data.added), removed: Some(!data.added),
}; };
ModAddCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?; let community_id = data.community_id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let res = AddModToCommunityResponse { moderators }; let res = AddModToCommunityResponse { moderators };
@ -724,14 +773,15 @@ impl Perform for Oper<AddModToCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<TransferCommunity> { impl Perform for Oper<TransferCommunity> {
type Response = GetCommunityResponse; type Response = GetCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetCommunityResponse, Error> { ) -> Result<GetCommunityResponse, LemmyError> {
let data: &TransferCommunity = &self.data; let data: &TransferCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -741,12 +791,14 @@ impl Perform for Oper<TransferCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let community_id = data.community_id;
let read_community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let read_community = Community::read(&conn, data.community_id)?; let site_creator_id =
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let site_creator_id = Site::read(&conn, 1)?.creator_id;
let mut admins = UserView::admins(&conn)?;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
@ -774,13 +826,18 @@ impl Perform for Oper<TransferCommunity> {
published: None, published: None,
}; };
let _updated_community = match Community::update(&conn, data.community_id, &community_form) { let community_id = data.community_id;
Ok(community) => community, let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form);
Err(_e) => return Err(APIError::err("couldnt_update_community").into()), if blocking(pool, update).await?.is_err() {
return Err(APIError::err("couldnt_update_community").into());
}; };
// You also have to re-do the community_moderator table, reordering it. // You also have to re-do the community_moderator table, reordering it.
let mut community_mods = CommunityModeratorView::for_community(&conn, data.community_id)?; let community_id = data.community_id;
let mut community_mods = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let creator_index = community_mods let creator_index = community_mods
.iter() .iter()
.position(|r| r.user_id == data.user_id) .position(|r| r.user_id == data.user_id)
@ -788,19 +845,23 @@ impl Perform for Oper<TransferCommunity> {
let creator_user = community_mods.remove(creator_index); let creator_user = community_mods.remove(creator_index);
community_mods.insert(0, creator_user); community_mods.insert(0, creator_user);
CommunityModerator::delete_for_community(&conn, data.community_id)?; let community_id = data.community_id;
blocking(pool, move |conn| {
CommunityModerator::delete_for_community(conn, community_id)
})
.await??;
// TODO: this should probably be a bulk operation
for cmod in &community_mods { for cmod in &community_mods {
let community_moderator_form = CommunityModeratorForm { let community_moderator_form = CommunityModeratorForm {
community_id: cmod.community_id, community_id: cmod.community_id,
user_id: cmod.user_id, user_id: cmod.user_id,
}; };
let _inserted_community_moderator = let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
match CommunityModerator::join(&conn, &community_moderator_form) { if blocking(pool, join).await?.is_err() {
Ok(user) => user, return Err(APIError::err("community_moderator_already_exists").into());
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }
};
} }
// Mod tables // Mod tables
@ -810,14 +871,24 @@ impl Perform for Oper<TransferCommunity> {
community_id: data.community_id, community_id: data.community_id,
removed: Some(false), removed: Some(false),
}; };
ModAddCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) { let community_id = data.community_id;
let community_view = match blocking(pool, move |conn| {
CommunityView::read(conn, community_id, Some(user_id))
})
.await?
{
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) { let community_id = data.community_id;
let moderators = match blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators, Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };

View file

@ -1,12 +1,10 @@
use crate::{ use crate::{
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*}, db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
websocket::WebsocketInfo, websocket::WebsocketInfo,
DbPool,
LemmyError,
}; };
use diesel::{ use actix_web::client::Client;
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
pub mod comment; pub mod comment;
pub mod community; pub mod community;
@ -30,20 +28,22 @@ impl APIError {
pub struct Oper<T> { pub struct Oper<T> {
data: T, data: T,
client: Client,
} }
impl<Data> Oper<Data> { impl<Data> Oper<Data> {
pub fn new(data: Data) -> Oper<Data> { pub fn new(data: Data, client: Client) -> Oper<Data> {
Oper { data } Oper { data, client }
} }
} }
#[async_trait::async_trait(?Send)]
pub trait Perform { pub trait Perform {
type Response: serde::ser::Serialize + Send; type Response: serde::ser::Serialize + Send;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<Self::Response, Error>; ) -> Result<Self::Response, LemmyError>;
} }

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking,
db::{ db::{
comment_view::*, comment_view::*,
community_view::*, community_view::*,
@ -26,12 +27,9 @@ use crate::{
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool,
LemmyError,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -112,14 +110,15 @@ pub struct SavePost {
auth: String, auth: String,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreatePost> { impl Perform for Oper<CreatePost> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &CreatePost = &self.data; let data: &CreatePost = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -139,22 +138,23 @@ impl Perform for Oper<CreatePost> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { let community_id = data.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Fetch Iframely and pictrs cached image // Fetch Iframely and pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(data.url.to_owned()); fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -177,7 +177,7 @@ impl Perform for Oper<CreatePost> {
published: None, published: None,
}; };
let inserted_post = match Post::create(&conn, &post_form) { let inserted_post = match blocking(pool, move |conn| Post::create(conn, &post_form)).await? {
Ok(post) => post, Ok(post) => post,
Err(e) => { Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" { let err_type = if e.to_string() == "value too long for type character varying(200)" {
@ -190,12 +190,14 @@ impl Perform for Oper<CreatePost> {
} }
}; };
let updated_post = match Post::update_ap_id(&conn, inserted_post.id) { let inserted_post_id = inserted_post.id;
Ok(post) => post, let updated_post =
Err(_e) => return Err(APIError::err("couldnt_create_post").into()), 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()),
};
updated_post.send_create(&user, &conn)?; updated_post.send_create(&user, &self.client, pool).await?;
// They like their own post by default // They like their own post by default
let like_form = PostLikeForm { let like_form = PostLikeForm {
@ -204,15 +206,20 @@ impl Perform for Oper<CreatePost> {
score: 1, score: 1,
}; };
let _inserted_like = match PostLike::like(&conn, &like_form) { let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
Ok(like) => like, if blocking(pool, like).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_like_post").into()), return Err(APIError::err("couldnt_like_post").into());
}; }
updated_post.send_like(&user, &conn)?; updated_post.send_like(&user, &self.client, pool).await?;
// Refetch the view // Refetch the view
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) { let inserted_post_id = inserted_post.id;
let post_view = match blocking(pool, move |conn| {
PostView::read(conn, inserted_post_id, Some(user_id))
})
.await?
{
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()), Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
}; };
@ -231,14 +238,15 @@ impl Perform for Oper<CreatePost> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetPost> { impl Perform for Oper<GetPost> {
type Response = GetPostResponse; type Response = GetPostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetPostResponse, Error> { ) -> Result<GetPostResponse, LemmyError> {
let data: &GetPost = &self.data; let data: &GetPost = &self.data;
let user_id: Option<i32> = match &data.auth { let user_id: Option<i32> = match &data.auth {
@ -252,25 +260,38 @@ impl Perform for Oper<GetPost> {
None => None, None => None,
}; };
let conn = pool.get()?; let id = data.id;
let post_view = match blocking(pool, move |conn| PostView::read(conn, id, user_id)).await? {
let post_view = match PostView::read(&conn, data.id, user_id) {
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()), Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
}; };
let comments = CommentQueryBuilder::create(&conn) let id = data.id;
.for_post_id(data.id) let comments = blocking(pool, move |conn| {
.my_user_id(user_id) CommentQueryBuilder::create(conn)
.limit(9999) .for_post_id(id)
.list()?; .my_user_id(user_id)
.limit(9999)
.list()
})
.await??;
let community = CommunityView::read(&conn, post_view.community_id, user_id)?; let community_id = post_view.community_id;
let community = blocking(pool, move |conn| {
CommunityView::read(conn, community_id, user_id)
})
.await??;
let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id)?; let community_id = post_view.community_id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let site_creator_id = Site::read(&conn, 1)?.creator_id; let site_creator_id =
let mut admins = UserView::admins(&conn)?; blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
@ -305,14 +326,15 @@ impl Perform for Oper<GetPost> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetPosts> { impl Perform for Oper<GetPosts> {
type Response = GetPostsResponse; type Response = GetPostsResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetPostsResponse, Error> { ) -> Result<GetPostsResponse, LemmyError> {
let data: &GetPosts = &self.data; let data: &GetPosts = &self.data;
let user_claims: Option<Claims> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
@ -336,17 +358,21 @@ impl Perform for Oper<GetPosts> {
let type_ = ListingType::from_str(&data.type_)?; let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let conn = pool.get()?; let page = data.page;
let limit = data.limit;
let posts = match PostQueryBuilder::create(&conn) let community_id = data.community_id;
.listing_type(type_) let posts = match blocking(pool, move |conn| {
.sort(&sort) PostQueryBuilder::create(conn)
.show_nsfw(show_nsfw) .listing_type(type_)
.for_community_id(data.community_id) .sort(&sort)
.my_user_id(user_id) .show_nsfw(show_nsfw)
.page(data.page) .for_community_id(community_id)
.limit(data.limit) .my_user_id(user_id)
.list() .page(page)
.limit(limit)
.list()
})
.await?
{ {
Ok(posts) => posts, Ok(posts) => posts,
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()), Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
@ -370,14 +396,15 @@ impl Perform for Oper<GetPosts> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreatePostLike> { impl Perform for Oper<CreatePostLike> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &CreatePostLike = &self.data; let data: &CreatePostLike = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -387,24 +414,27 @@ impl Perform for Oper<CreatePostLike> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Don't do a downvote if site has downvotes disabled // Don't do a downvote if site has downvotes disabled
if data.score == -1 { if data.score == -1 {
let site = SiteView::read(&conn)?; let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
if !site.enable_downvotes { if !site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into()); return Err(APIError::err("downvotes_disabled").into());
} }
} }
// Check for a community ban // Check for a community ban
let post = Post::read(&conn, data.post_id)?; let post_id = data.post_id;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
@ -416,26 +446,33 @@ impl Perform for Oper<CreatePostLike> {
}; };
// Remove any likes first // Remove any likes first
PostLike::remove(&conn, &like_form)?; let like_form2 = like_form.clone();
blocking(pool, move |conn| PostLike::remove(conn, &like_form2)).await??;
// Only add the like if the score isnt 0 // Only add the like if the score isnt 0
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add { if do_add {
let _inserted_like = match PostLike::like(&conn, &like_form) { let like_form2 = like_form.clone();
Ok(like) => like, let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
Err(_e) => return Err(APIError::err("couldnt_like_post").into()), if blocking(pool, like).await?.is_err() {
}; return Err(APIError::err("couldnt_like_post").into());
}
if like_form.score == 1 { if like_form.score == 1 {
post.send_like(&user, &conn)?; post.send_like(&user, &self.client, pool).await?;
} else if like_form.score == -1 { } else if like_form.score == -1 {
post.send_dislike(&user, &conn)?; post.send_dislike(&user, &self.client, pool).await?;
} }
} else { } else {
post.send_undo_like(&user, &conn)?; post.send_undo_like(&user, &self.client, pool).await?;
} }
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) { let post_id = data.post_id;
let post_view = match blocking(pool, move |conn| {
PostView::read(conn, post_id, Some(user_id))
})
.await?
{
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()), Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
}; };
@ -454,14 +491,15 @@ impl Perform for Oper<CreatePostLike> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditPost> { impl Perform for Oper<EditPost> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &EditPost = &self.data; let data: &EditPost = &self.data;
if let Err(slurs) = slur_check(&data.name) { if let Err(slurs) = slur_check(&data.name) {
@ -481,37 +519,46 @@ impl Perform for Oper<EditPost> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Verify its the creator or a mod or admin // Verify its the creator or a mod or admin
let community_id = data.community_id;
let mut editors: Vec<i32> = vec![data.creator_id]; let mut editors: Vec<i32> = vec![data.creator_id];
editors.append( editors.append(
&mut CommunityModeratorView::for_community(&conn, data.community_id)? &mut blocking(pool, move |conn| {
.into_iter() CommunityModeratorView::for_community(conn, community_id)
.map(|m| m.user_id) .map(|v| v.into_iter().map(|m| m.user_id).collect())
.collect(), })
.await??,
);
editors.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
); );
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err("no_post_edit_allowed").into()); return Err(APIError::err("no_post_edit_allowed").into());
} }
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { let community_id = data.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Fetch Iframely and Pictrs cached image // Fetch Iframely and Pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(data.url.to_owned()); fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let read_post = Post::read(&conn, data.edit_id)?; let edit_id = data.edit_id;
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -534,7 +581,9 @@ impl Perform for Oper<EditPost> {
published: None, published: None,
}; };
let updated_post = match Post::update(&conn, data.edit_id, &post_form) { let edit_id = data.edit_id;
let res = blocking(pool, move |conn| Post::update(conn, edit_id, &post_form)).await?;
let updated_post: Post = match res {
Ok(post) => post, Ok(post) => post,
Err(e) => { Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" { let err_type = if e.to_string() == "value too long for type character varying(200)" {
@ -555,7 +604,7 @@ impl Perform for Oper<EditPost> {
removed: Some(removed), removed: Some(removed),
reason: data.reason.to_owned(), reason: data.reason.to_owned(),
}; };
ModRemovePost::create(&conn, &form)?; blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
} }
if let Some(locked) = data.locked.to_owned() { if let Some(locked) = data.locked.to_owned() {
@ -564,7 +613,7 @@ impl Perform for Oper<EditPost> {
post_id: data.edit_id, post_id: data.edit_id,
locked: Some(locked), locked: Some(locked),
}; };
ModLockPost::create(&conn, &form)?; blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
} }
if let Some(stickied) = data.stickied.to_owned() { if let Some(stickied) = data.stickied.to_owned() {
@ -573,26 +622,34 @@ impl Perform for Oper<EditPost> {
post_id: data.edit_id, post_id: data.edit_id,
stickied: Some(stickied), stickied: Some(stickied),
}; };
ModStickyPost::create(&conn, &form)?; blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
} }
if let Some(deleted) = data.deleted.to_owned() { if let Some(deleted) = data.deleted.to_owned() {
if deleted { if deleted {
updated_post.send_delete(&user, &conn)?; updated_post.send_delete(&user, &self.client, pool).await?;
} else { } else {
updated_post.send_undo_delete(&user, &conn)?; updated_post
.send_undo_delete(&user, &self.client, pool)
.await?;
} }
} else if let Some(removed) = data.removed.to_owned() { } else if let Some(removed) = data.removed.to_owned() {
if removed { if removed {
updated_post.send_remove(&user, &conn)?; updated_post.send_remove(&user, &self.client, pool).await?;
} else { } else {
updated_post.send_undo_remove(&user, &conn)?; updated_post
.send_undo_remove(&user, &self.client, pool)
.await?;
} }
} else { } else {
updated_post.send_update(&user, &conn)?; updated_post.send_update(&user, &self.client, pool).await?;
} }
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?; let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view }; let res = PostResponse { post: post_view };
@ -608,14 +665,15 @@ impl Perform for Oper<EditPost> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SavePost> { impl Perform for Oper<SavePost> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &SavePost = &self.data; let data: &SavePost = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -630,21 +688,23 @@ impl Perform for Oper<SavePost> {
user_id, user_id,
}; };
let conn = pool.get()?;
if data.save { if data.save {
match PostSaved::save(&conn, &post_saved_form) { let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
Ok(post) => post, if blocking(pool, save).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_post").into()), return Err(APIError::err("couldnt_save_post").into());
}; }
} else { } else {
match PostSaved::unsave(&conn, &post_saved_form) { let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
Ok(post) => post, if blocking(pool, unsave).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_post").into()), return Err(APIError::err("couldnt_save_post").into());
}; }
} }
let post_view = PostView::read(&conn, data.post_id, Some(user_id))?; let post_id = data.post_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, post_id, Some(user_id))
})
.await??;
Ok(PostResponse { post: post_view }) Ok(PostResponse { post: post_view })
} }

View file

@ -2,6 +2,7 @@ use super::user::Register;
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{APIError, Oper, Perform},
apub::fetcher::search_by_apub_id, apub::fetcher::search_by_apub_id,
blocking,
db::{ db::{
category::*, category::*,
comment_view::*, comment_view::*,
@ -22,12 +23,9 @@ use crate::{
slur_check, slur_check,
slurs_vec_to_str, slurs_vec_to_str,
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo}, websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
DbPool,
LemmyError,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::{debug, info}; use log::{debug, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -139,87 +137,79 @@ pub struct SaveSiteConfig {
auth: String, auth: String,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<ListCategories> { impl Perform for Oper<ListCategories> {
type Response = ListCategoriesResponse; type Response = ListCategoriesResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<ListCategoriesResponse, Error> { ) -> Result<ListCategoriesResponse, LemmyError> {
let _data: &ListCategories = &self.data; let _data: &ListCategories = &self.data;
let conn = pool.get()?; let categories = blocking(pool, move |conn| Category::list_all(conn)).await??;
let categories: Vec<Category> = Category::list_all(&conn)?;
// Return the jwt // Return the jwt
Ok(ListCategoriesResponse { categories }) Ok(ListCategoriesResponse { categories })
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetModlog> { impl Perform for Oper<GetModlog> {
type Response = GetModlogResponse; type Response = GetModlogResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetModlogResponse, Error> { ) -> Result<GetModlogResponse, LemmyError> {
let data: &GetModlog = &self.data; let data: &GetModlog = &self.data;
let conn = pool.get()?; let community_id = data.community_id;
let mod_user_id = data.mod_user_id;
let page = data.page;
let limit = data.limit;
let removed_posts = blocking(pool, move |conn| {
ModRemovePostView::list(conn, community_id, mod_user_id, page, limit)
})
.await??;
let removed_posts = ModRemovePostView::list( let locked_posts = blocking(pool, move |conn| {
&conn, ModLockPostView::list(conn, community_id, mod_user_id, page, limit)
data.community_id, })
data.mod_user_id, .await??;
data.page,
data.limit, let stickied_posts = blocking(pool, move |conn| {
)?; ModStickyPostView::list(conn, community_id, mod_user_id, page, limit)
let locked_posts = ModLockPostView::list( })
&conn, .await??;
data.community_id,
data.mod_user_id, let removed_comments = blocking(pool, move |conn| {
data.page, ModRemoveCommentView::list(conn, community_id, mod_user_id, page, limit)
data.limit, })
)?; .await??;
let stickied_posts = ModStickyPostView::list(
&conn, let banned_from_community = blocking(pool, move |conn| {
data.community_id, ModBanFromCommunityView::list(conn, community_id, mod_user_id, page, limit)
data.mod_user_id, })
data.page, .await??;
data.limit,
)?; let added_to_community = blocking(pool, move |conn| {
let removed_comments = ModRemoveCommentView::list( ModAddCommunityView::list(conn, community_id, mod_user_id, page, limit)
&conn, })
data.community_id, .await??;
data.mod_user_id,
data.page,
data.limit,
)?;
let banned_from_community = ModBanFromCommunityView::list(
&conn,
data.community_id,
data.mod_user_id,
data.page,
data.limit,
)?;
let added_to_community = ModAddCommunityView::list(
&conn,
data.community_id,
data.mod_user_id,
data.page,
data.limit,
)?;
// These arrays are only for the full modlog, when a community isn't given // These arrays are only for the full modlog, when a community isn't given
let (removed_communities, banned, added) = if data.community_id.is_none() { let (removed_communities, banned, added) = if data.community_id.is_none() {
( blocking(pool, move |conn| {
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?, Ok((
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?, ModRemoveCommunityView::list(conn, mod_user_id, page, limit)?,
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?, ModBanView::list(conn, mod_user_id, page, limit)?,
) ModAddView::list(conn, mod_user_id, page, limit)?,
)) as Result<_, LemmyError>
})
.await??
} else { } else {
(Vec::new(), Vec::new(), Vec::new()) (Vec::new(), Vec::new(), Vec::new())
}; };
@ -239,14 +229,15 @@ impl Perform for Oper<GetModlog> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateSite> { impl Perform for Oper<CreateSite> {
type Response = SiteResponse; type Response = SiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<SiteResponse, Error> { ) -> Result<SiteResponse, LemmyError> {
let data: &CreateSite = &self.data; let data: &CreateSite = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -266,10 +257,9 @@ impl Perform for Oper<CreateSite> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Make sure user is an admin // Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin { let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into()); return Err(APIError::err("not_an_admin").into());
} }
@ -283,24 +273,25 @@ impl Perform for Oper<CreateSite> {
updated: None, updated: None,
}; };
match Site::create(&conn, &site_form) { let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
Ok(site) => site, if blocking(pool, create_site).await?.is_err() {
Err(_e) => return Err(APIError::err("site_already_exists").into()), return Err(APIError::err("site_already_exists").into());
}; }
let site_view = SiteView::read(&conn)?; let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
Ok(SiteResponse { site: site_view }) Ok(SiteResponse { site: site_view })
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditSite> { impl Perform for Oper<EditSite> {
type Response = SiteResponse; type Response = SiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<SiteResponse, Error> { ) -> Result<SiteResponse, LemmyError> {
let data: &EditSite = &self.data; let data: &EditSite = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -320,14 +311,13 @@ impl Perform for Oper<EditSite> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Make sure user is an admin // Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin { let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into()); return Err(APIError::err("not_an_admin").into());
} }
let found_site = Site::read(&conn, 1)?; let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let site_form = SiteForm { let site_form = SiteForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -339,12 +329,12 @@ impl Perform for Oper<EditSite> {
enable_nsfw: data.enable_nsfw, enable_nsfw: data.enable_nsfw,
}; };
match Site::update(&conn, 1, &site_form) { let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
Ok(site) => site, if blocking(pool, update_site).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_update_site").into()), return Err(APIError::err("couldnt_update_site").into());
}; }
let site_view = SiteView::read(&conn)?; let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
let res = SiteResponse { site: site_view }; let res = SiteResponse { site: site_view };
@ -360,21 +350,21 @@ impl Perform for Oper<EditSite> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetSite> { impl Perform for Oper<GetSite> {
type Response = GetSiteResponse; type Response = GetSiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteResponse, Error> { ) -> Result<GetSiteResponse, LemmyError> {
let _data: &GetSite = &self.data; let _data: &GetSite = &self.data;
let conn = pool.get()?;
// TODO refactor this a little // TODO refactor this a little
let site_view = if let Ok(_site) = Site::read(&conn, 1) { let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
Some(SiteView::read(&conn)?) let site_view = if res.is_ok() {
Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
} else if let Some(setup) = Settings::get().setup.as_ref() { } else if let Some(setup) = Settings::get().setup.as_ref() {
let register = Register { let register = Register {
username: setup.admin_username.to_owned(), username: setup.admin_username.to_owned(),
@ -384,7 +374,9 @@ impl Perform for Oper<GetSite> {
admin: true, admin: true,
show_nsfw: true, show_nsfw: true,
}; };
let login_response = Oper::new(register).perform(pool.clone(), websocket_info.clone())?; let login_response = Oper::new(register, self.client.clone())
.perform(pool, websocket_info.clone())
.await?;
info!("Admin {} created", setup.admin_username); info!("Admin {} created", setup.admin_username);
let create_site = CreateSite { let create_site = CreateSite {
@ -395,14 +387,16 @@ impl Perform for Oper<GetSite> {
enable_nsfw: true, enable_nsfw: true,
auth: login_response.jwt, auth: login_response.jwt,
}; };
Oper::new(create_site).perform(pool, websocket_info.clone())?; Oper::new(create_site, self.client.clone())
.perform(pool, websocket_info.clone())
.await?;
info!("Site {} created", setup.site_name); info!("Site {} created", setup.site_name);
Some(SiteView::read(&conn)?) Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
} else { } else {
None None
}; };
let mut admins = UserView::admins(&conn)?; let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
// Make sure the site creator is the top admin // Make sure the site creator is the top admin
if let Some(site_view) = site_view.to_owned() { if let Some(site_view) = site_view.to_owned() {
@ -415,7 +409,7 @@ impl Perform for Oper<GetSite> {
} }
} }
let banned = UserView::banned(&conn)?; let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
let online = if let Some(_ws) = websocket_info { let online = if let Some(_ws) = websocket_info {
// TODO // TODO
@ -437,21 +431,20 @@ impl Perform for Oper<GetSite> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<Search> { impl Perform for Oper<Search> {
type Response = SearchResponse; type Response = SearchResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<SearchResponse, Error> { ) -> Result<SearchResponse, LemmyError> {
let data: &Search = &self.data; let data: &Search = &self.data;
dbg!(&data); dbg!(&data);
let conn = pool.get()?; match search_by_apub_id(&data.q, &self.client, pool).await {
match search_by_apub_id(&data.q, &conn) {
Ok(r) => return Ok(r), Ok(r) => return Ok(r),
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e), Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
} }
@ -467,7 +460,6 @@ impl Perform for Oper<Search> {
None => None, None => None,
}; };
let sort = SortType::from_str(&data.sort)?;
let type_ = SearchType::from_str(&data.type_)?; let type_ = SearchType::from_str(&data.type_)?;
let mut posts = Vec::new(); let mut posts = Vec::new();
@ -477,85 +469,126 @@ impl Perform for Oper<Search> {
// TODO no clean / non-nsfw searching rn // TODO no clean / non-nsfw searching rn
let q = data.q.to_owned();
let page = data.page;
let limit = data.limit;
let sort = SortType::from_str(&data.sort)?;
let community_id = data.community_id;
match type_ { match type_ {
SearchType::Posts => { SearchType::Posts => {
posts = PostQueryBuilder::create(&conn) posts = blocking(pool, move |conn| {
.sort(&sort) PostQueryBuilder::create(conn)
.show_nsfw(true) .sort(&sort)
.for_community_id(data.community_id) .show_nsfw(true)
.search_term(data.q.to_owned()) .for_community_id(community_id)
.my_user_id(user_id) .search_term(q)
.page(data.page) .my_user_id(user_id)
.limit(data.limit) .page(page)
.list()?; .limit(limit)
.list()
})
.await??;
} }
SearchType::Comments => { SearchType::Comments => {
comments = CommentQueryBuilder::create(&conn) comments = blocking(pool, move |conn| {
.sort(&sort) CommentQueryBuilder::create(&conn)
.search_term(data.q.to_owned()) .sort(&sort)
.my_user_id(user_id) .search_term(q)
.page(data.page) .my_user_id(user_id)
.limit(data.limit) .page(page)
.list()?; .limit(limit)
.list()
})
.await??;
} }
SearchType::Communities => { SearchType::Communities => {
communities = CommunityQueryBuilder::create(&conn) communities = blocking(pool, move |conn| {
.sort(&sort) CommunityQueryBuilder::create(conn)
.search_term(data.q.to_owned()) .sort(&sort)
.page(data.page) .search_term(q)
.limit(data.limit) .page(page)
.list()?; .limit(limit)
.list()
})
.await??;
} }
SearchType::Users => { SearchType::Users => {
users = UserQueryBuilder::create(&conn) users = blocking(pool, move |conn| {
.sort(&sort) UserQueryBuilder::create(conn)
.search_term(data.q.to_owned()) .sort(&sort)
.page(data.page) .search_term(q)
.limit(data.limit) .page(page)
.list()?; .limit(limit)
.list()
})
.await??;
} }
SearchType::All => { SearchType::All => {
posts = PostQueryBuilder::create(&conn) posts = blocking(pool, move |conn| {
.sort(&sort) PostQueryBuilder::create(conn)
.show_nsfw(true) .sort(&sort)
.for_community_id(data.community_id) .show_nsfw(true)
.search_term(data.q.to_owned()) .for_community_id(community_id)
.my_user_id(user_id) .search_term(q)
.page(data.page) .my_user_id(user_id)
.limit(data.limit) .page(page)
.list()?; .limit(limit)
.list()
})
.await??;
comments = CommentQueryBuilder::create(&conn) let q = data.q.to_owned();
.sort(&sort) let sort = SortType::from_str(&data.sort)?;
.search_term(data.q.to_owned())
.my_user_id(user_id)
.page(data.page)
.limit(data.limit)
.list()?;
communities = CommunityQueryBuilder::create(&conn) comments = blocking(pool, move |conn| {
.sort(&sort) CommentQueryBuilder::create(conn)
.search_term(data.q.to_owned()) .sort(&sort)
.page(data.page) .search_term(q)
.limit(data.limit) .my_user_id(user_id)
.list()?; .page(page)
.limit(limit)
.list()
})
.await??;
users = UserQueryBuilder::create(&conn) let q = data.q.to_owned();
.sort(&sort) let sort = SortType::from_str(&data.sort)?;
.search_term(data.q.to_owned())
.page(data.page) communities = blocking(pool, move |conn| {
.limit(data.limit) CommunityQueryBuilder::create(conn)
.list()?; .sort(&sort)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
users = blocking(pool, move |conn| {
UserQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
} }
SearchType::Url => { SearchType::Url => {
posts = PostQueryBuilder::create(&conn) posts = blocking(pool, move |conn| {
.sort(&sort) PostQueryBuilder::create(conn)
.show_nsfw(true) .sort(&sort)
.for_community_id(data.community_id) .show_nsfw(true)
.url_search(data.q.to_owned()) .for_community_id(community_id)
.page(data.page) .url_search(q)
.limit(data.limit) .page(page)
.list()?; .limit(limit)
.list()
})
.await??;
} }
}; };
@ -570,14 +603,15 @@ impl Perform for Oper<Search> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<TransferSite> { impl Perform for Oper<TransferSite> {
type Response = GetSiteResponse; type Response = GetSiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteResponse, Error> { ) -> Result<GetSiteResponse, LemmyError> {
let data: &TransferSite = &self.data; let data: &TransferSite = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -587,9 +621,7 @@ impl Perform for Oper<TransferSite> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let read_site = Site::read(&conn, 1)?;
// Make sure user is the creator // Make sure user is the creator
if read_site.creator_id != user_id { if read_site.creator_id != user_id {
@ -606,9 +638,9 @@ impl Perform for Oper<TransferSite> {
enable_nsfw: read_site.enable_nsfw, enable_nsfw: read_site.enable_nsfw,
}; };
match Site::update(&conn, 1, &site_form) { let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
Ok(site) => site, if blocking(pool, update_site).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_update_site").into()), return Err(APIError::err("couldnt_update_site").into());
}; };
// Mod tables // Mod tables
@ -618,11 +650,11 @@ impl Perform for Oper<TransferSite> {
removed: Some(false), removed: Some(false),
}; };
ModAdd::create(&conn, &form)?; blocking(pool, move |conn| ModAdd::create(conn, &form)).await??;
let site_view = SiteView::read(&conn)?; let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
let mut admins = UserView::admins(&conn)?; let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins let creator_index = admins
.iter() .iter()
.position(|r| r.id == site_view.creator_id) .position(|r| r.id == site_view.creator_id)
@ -630,7 +662,7 @@ impl Perform for Oper<TransferSite> {
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
let banned = UserView::banned(&conn)?; let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
Ok(GetSiteResponse { Ok(GetSiteResponse {
site: Some(site_view), site: Some(site_view),
@ -641,14 +673,15 @@ impl Perform for Oper<TransferSite> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetSiteConfig> { impl Perform for Oper<GetSiteConfig> {
type Response = GetSiteConfigResponse; type Response = GetSiteConfigResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteConfigResponse, Error> { ) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &GetSiteConfig = &self.data; let data: &GetSiteConfig = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -658,10 +691,8 @@ impl Perform for Oper<GetSiteConfig> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Only let admins read this // Only let admins read this
let admins = UserView::admins(&conn)?; let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect(); let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) { if !admin_ids.contains(&user_id) {
@ -674,14 +705,15 @@ impl Perform for Oper<GetSiteConfig> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SaveSiteConfig> { impl Perform for Oper<SaveSiteConfig> {
type Response = GetSiteConfigResponse; type Response = GetSiteConfigResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteConfigResponse, Error> { ) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &SaveSiteConfig = &self.data; let data: &SaveSiteConfig = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -691,10 +723,8 @@ impl Perform for Oper<SaveSiteConfig> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Only let admins read this // Only let admins read this
let admins = UserView::admins(&conn)?; let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect(); let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) { if !admin_ids.contains(&user_id) {

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,22 @@
use crate::{ use crate::{
apub::{extensions::signatures::sign, is_apub_id_valid, ActorType}, apub::{extensions::signatures::sign, is_apub_id_valid, ActorType},
db::{activity::insert_activity, community::Community, user::User_}, db::{activity::insert_activity, community::Community, user::User_},
request::retry_custom,
DbPool,
LemmyError,
}; };
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
use diesel::PgConnection; use actix_web::client::Client;
use failure::{Error, _core::fmt::Debug};
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug;
use url::Url; use url::Url;
pub fn populate_object_props( pub fn populate_object_props(
props: &mut ObjectProperties, props: &mut ObjectProperties,
addressed_ccs: Vec<String>, addressed_ccs: Vec<String>,
object_id: &str, object_id: &str,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
props props
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
// TODO: the activity needs a seperate id from the object // TODO: the activity needs a seperate id from the object
@ -26,48 +28,61 @@ pub fn populate_object_props(
Ok(()) Ok(())
} }
pub fn send_activity_to_community<A>( pub async fn send_activity_to_community<A>(
creator: &User_, creator: &User_,
conn: &PgConnection,
community: &Community, community: &Community,
to: Vec<String>, to: Vec<String>,
activity: A, activity: A,
) -> Result<(), Error> client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>
where where
A: Activity + Base + Serialize + Debug, A: Activity + Base + Serialize + Debug + Clone + Send + 'static,
{ {
insert_activity(&conn, creator.id, &activity, true)?; insert_activity(creator.id, activity.clone(), true, pool).await?;
// if this is a local community, we need to do an announce from the community instead // if this is a local community, we need to do an announce from the community instead
if community.local { if community.local {
Community::do_announce(activity, &community, creator, conn)?; Community::do_announce(activity, &community, creator, client, pool).await?;
} else { } else {
send_activity(&activity, creator, to)?; send_activity(client, &activity, creator, to).await?;
} }
Ok(()) Ok(())
} }
/// Send an activity to a list of recipients, using the correct headers etc. /// Send an activity to a list of recipients, using the correct headers etc.
pub fn send_activity<A>(activity: &A, actor: &dyn ActorType, to: Vec<String>) -> Result<(), Error> pub async fn send_activity<A>(
client: &Client,
activity: &A,
actor: &dyn ActorType,
to: Vec<String>,
) -> Result<(), LemmyError>
where where
A: Serialize + Debug, A: Serialize,
{ {
let json = serde_json::to_string(&activity)?; let activity = serde_json::to_string(&activity)?;
debug!("Sending activitypub activity {} to {:?}", json, to); debug!("Sending activitypub activity {} to {:?}", activity, to);
for t in to { for t in to {
let to_url = Url::parse(&t)?; let to_url = Url::parse(&t)?;
if !is_apub_id_valid(&to_url) { if !is_apub_id_valid(&to_url) {
debug!("Not sending activity to {} (invalid or blocklisted)", t); debug!("Not sending activity to {} (invalid or blocklisted)", t);
continue; continue;
} }
let request = Request::post(t).header("Host", to_url.domain().unwrap());
let signature = sign(&request, actor)?; let res = retry_custom(|| async {
let res = request let request = client.post(&t).header("Content-Type", "application/json");
.header("Signature", signature)
.header("Content-Type", "application/json") match sign(request, actor, activity.clone()).await {
.body(json.to_owned())? Ok(signed) => Ok(signed.send().await),
.send()?; Err(e) => Err(e),
}
})
.await?;
debug!("Result for activity send: {:?}", res); debug!("Result for activity send: {:?}", res);
} }
Ok(()) Ok(())
} }

View file

@ -16,6 +16,7 @@ use crate::{
FromApub, FromApub,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
comment::{Comment, CommentForm}, comment::{Comment, CommentForm},
@ -26,6 +27,8 @@ use crate::{
}, },
routes::DbPoolParam, routes::DbPoolParam,
scrape_text_for_mentions, scrape_text_for_mentions,
DbPool,
LemmyError,
MentionData, MentionData,
}; };
use activitystreams::{ use activitystreams::{
@ -35,9 +38,7 @@ use activitystreams::{
object::{kind::NoteType, properties::ObjectProperties, Note}, object::{kind::NoteType, properties::ObjectProperties, Note},
}; };
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
use diesel::PgConnection;
use failure::Error;
use itertools::Itertools; use itertools::Itertools;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
@ -51,32 +52,41 @@ pub struct CommentQuery {
pub async fn get_apub_comment( pub async fn get_apub_comment(
info: Path<CommentQuery>, info: Path<CommentQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let id = info.comment_id.parse::<i32>()?; let id = info.comment_id.parse::<i32>()?;
let comment = Comment::read(&&db.get()?, id)?; let comment = blocking(&db, move |conn| Comment::read(conn, id)).await??;
if !comment.deleted { if !comment.deleted {
Ok(create_apub_response(&comment.to_apub(&db.get().unwrap())?)) Ok(create_apub_response(&comment.to_apub(&db).await?))
} else { } else {
Ok(create_apub_tombstone_response(&comment.to_tombstone()?)) Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
} }
} }
#[async_trait::async_trait(?Send)]
impl ToApub for Comment { impl ToApub for Comment {
type Response = Note; type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut comment = Note::default(); let mut comment = Note::default();
let oprops: &mut ObjectProperties = comment.as_mut(); let oprops: &mut ObjectProperties = comment.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let post = Post::read(&conn, self.post_id)?; let creator_id = self.creator_id;
let community = Community::read(&conn, post.community_id)?; let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Add a vector containing some important info to the "in_reply_to" field // Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)] // [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id]; let mut in_reply_to_vec = vec![post.ap_id];
if let Some(parent_id) = self.parent_id { if let Some(parent_id) = self.parent_id {
let parent_comment = Comment::read(&conn, parent_id)?; let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
in_reply_to_vec.push(parent_comment.ap_id); in_reply_to_vec.push(parent_comment.ap_id);
} }
@ -97,7 +107,7 @@ impl ToApub for Comment {
Ok(comment) Ok(comment)
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
@ -107,27 +117,34 @@ impl ToApub for Comment {
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for CommentForm { impl FromApub for CommentForm {
type ApubType = Note; type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy comment /// Parse an ActivityPub note received from another instance into a Lemmy comment
fn from_apub(note: &Note, conn: &PgConnection) -> Result<CommentForm, Error> { async fn from_apub(
note: &Note,
client: &Client,
pool: &DbPool,
) -> Result<CommentForm, LemmyError> {
let oprops = &note.object_props; let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap(); let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
let post_ap_id = in_reply_tos.next().unwrap().to_string(); let post_ap_id = in_reply_tos.next().unwrap().to_string();
// This post, or the parent comment might not yet exist on this server yet, fetch them. // This post, or the parent comment might not yet exist on this server yet, fetch them.
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
// The 2nd item, if it exists, is the parent comment apub_id // The 2nd item, if it exists, is the parent comment apub_id
// For deeply nested comments, FromApub automatically gets called recursively // For deeply nested comments, FromApub automatically gets called recursively
let parent_id: Option<i32> = match in_reply_tos.next() { let parent_id: Option<i32> = match in_reply_tos.next() {
Some(parent_comment_uri) => { Some(parent_comment_uri) => {
let parent_comment_ap_id = &parent_comment_uri.to_string(); let parent_comment_ap_id = &parent_comment_uri.to_string();
let parent_comment = get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, &conn)?; let parent_comment =
get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?;
Some(parent_comment.id) Some(parent_comment.id)
} }
@ -157,17 +174,27 @@ impl FromApub for CommentForm {
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Comment { impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community. /// Send out information about a newly created comment, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_create(
let note = self.to_apub(conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let maa =
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let maa: MentionsAndAddresses =
collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
let mut create = Create::new(); let mut create = Create::new();
populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?; populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?;
@ -179,20 +206,29 @@ impl ApubObjectType for Comment {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
send_activity_to_community(&creator, &conn, &community, maa.inboxes, create)?; send_activity_to_community(&creator, &community, maa.inboxes, create, client, pool).await?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_update(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let maa =
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let maa: MentionsAndAddresses =
collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
let mut update = Update::new(); let mut update = Update::new();
populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?; populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?;
@ -204,14 +240,24 @@ impl ApubObjectType for Comment {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
send_activity_to_community(&creator, &conn, &community, maa.inboxes, update)?; send_activity_to_community(&creator, &community, maa.inboxes, update, client, pool).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -228,18 +274,29 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
delete, delete,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Generate a fake delete activity, with the correct object // Generate a fake delete activity, with the correct object
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
@ -274,18 +331,30 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; mod_: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -302,18 +371,29 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&mod_, &mod_,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
remove, remove,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; mod_: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Generate a fake delete activity, with the correct object // Generate a fake delete activity, with the correct object
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
@ -347,20 +427,33 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&mod_, &mod_,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Comment { impl ApubLikeableType for Comment {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_like(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -376,18 +469,30 @@ impl ApubLikeableType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
like, like,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_dislike(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new(); let mut dislike = Dislike::new();
@ -403,18 +508,30 @@ impl ApubLikeableType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
dislike, dislike,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_like(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -446,11 +563,13 @@ impl ApubLikeableType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }
@ -464,11 +583,12 @@ struct MentionsAndAddresses {
/// This takes a comment, and builds a list of to_addresses, inboxes, /// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to. /// and mention tags, so they know where to be sent to.
/// Addresses are the users / addresses that go in the cc field. /// Addresses are the users / addresses that go in the cc field.
fn collect_non_local_mentions_and_addresses( async fn collect_non_local_mentions_and_addresses(
conn: &PgConnection,
content: &str, content: &str,
community: &Community, community: &Community,
) -> Result<MentionsAndAddresses, Error> { client: &Client,
pool: &DbPool,
) -> Result<MentionsAndAddresses, LemmyError> {
let mut addressed_ccs = vec![community.get_followers_url()]; let mut addressed_ccs = vec![community.get_followers_url()];
// Add the mention tag // Add the mention tag
@ -480,14 +600,17 @@ fn collect_non_local_mentions_and_addresses(
// Filter only the non-local ones // Filter only the non-local ones
.filter(|m| !m.is_local()) .filter(|m| !m.is_local())
.collect::<Vec<MentionData>>(); .collect::<Vec<MentionData>>();
let mut mention_inboxes = Vec::new(); let mut mention_inboxes = Vec::new();
for mention in &mentions { for mention in &mentions {
// TODO should it be fetching it every time? // TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention) { if let Ok(actor_id) = fetch_webfinger_url(mention, client).await {
debug!("mention actor_id: {}", actor_id); debug!("mention actor_id: {}", actor_id);
addressed_ccs.push(actor_id.to_owned()); addressed_ccs.push(actor_id.to_owned());
let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, &conn)?;
let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?;
let shared_inbox = mention_user.get_shared_inbox_url(); let shared_inbox = mention_user.get_shared_inbox_url();
mention_inboxes.push(shared_inbox); mention_inboxes.push(shared_inbox);
let mut mention_tag = Mention::new(); let mut mention_tag = Mention::new();
mention_tag mention_tag

View file

@ -12,6 +12,7 @@ use crate::{
GroupExt, GroupExt,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
activity::insert_activity, activity::insert_activity,
@ -21,6 +22,8 @@ use crate::{
}, },
naive_now, naive_now,
routes::DbPoolParam, routes::DbPoolParam,
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, Announce, Delete, Remove, Undo}, activity::{Accept, Announce, Delete, Remove, Undo},
@ -35,22 +38,22 @@ use activitystreams::{
}; };
use activitystreams_ext::Ext3; use activitystreams_ext::Ext3;
use activitystreams_new::{activity::Follow, object::Tombstone}; use activitystreams_new::{activity::Follow, object::Tombstone};
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub struct CommunityQuery {
community_name: String, community_name: String,
} }
#[async_trait::async_trait(?Send)]
impl ToApub for Community { impl ToApub for Community {
type Response = GroupExt; type Response = GroupExt;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn to_apub(&self, conn: &PgConnection) -> Result<GroupExt, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
let mut group = Group::default(); let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut(); let oprops: &mut ObjectProperties = group.as_mut();
@ -58,10 +61,12 @@ impl ToApub for Community {
// then the rest of the moderators // then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets // TODO Technically the instance admins can mod the community, but lets
// ignore that for now // ignore that for now
let moderators = CommunityModeratorView::for_community(&conn, self.id)? let id = self.id;
.into_iter() let moderators = blocking(pool, move |conn| {
.map(|m| m.user_actor_id) CommunityModeratorView::for_community(&conn, id)
.collect(); })
.await??;
let moderators = moderators.into_iter().map(|m| m.user_actor_id).collect();
oprops oprops
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
@ -92,7 +97,12 @@ impl ToApub for Community {
.set_endpoints(endpoint_props)? .set_endpoints(endpoint_props)?
.set_followers(self.get_followers_url())?; .set_followers(self.get_followers_url())?;
let group_extension = GroupExtension::new(conn, self.category_id, self.nsfw)?; let nsfw = self.nsfw;
let category_id = self.category_id;
let group_extension = blocking(pool, move |conn| {
GroupExtension::new(conn, category_id, nsfw)
})
.await??;
Ok(Ext3::new( Ok(Ext3::new(
group, group,
@ -102,7 +112,7 @@ impl ToApub for Community {
)) ))
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.actor_id, &self.actor_id,
@ -112,6 +122,7 @@ impl ToApub for Community {
} }
} }
#[async_trait::async_trait(?Send)]
impl ActorType for Community { impl ActorType for Community {
fn actor_id(&self) -> String { fn actor_id(&self) -> String {
self.actor_id.to_owned() self.actor_id.to_owned()
@ -125,7 +136,12 @@ impl ActorType for Community {
} }
/// As a local community, accept the follow request from a remote user. /// As a local community, accept the follow request from a remote user.
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> { async fn send_accept_follow(
&self,
follow: &Follow,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4());
@ -140,14 +156,20 @@ impl ActorType for Community {
.set_object_base_box(BaseBox::from_concrete(follow.clone())?)?; .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
let to = format!("{}/inbox", actor_uri); let to = format!("{}/inbox", actor_uri);
insert_activity(&conn, self.creator_id, &accept, true)?; insert_activity(self.creator_id, accept.clone(), true, pool).await?;
send_activity(&accept, self, vec![to])?; send_activity(client, &accept, self, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let group = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -162,17 +184,25 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_object_base_box(BaseBox::from_concrete(group)?)?;
insert_activity(&conn, self.creator_id, &delete, true)?; insert_activity(self.creator_id, delete.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // But for delete, the creator is the actor, and does the signing
send_activity(&delete, creator, self.get_follower_inboxes(&conn)?)?; send_activity(client, &delete, creator, inboxes).await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let group = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -203,17 +233,25 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?; .set_object_base_box(delete)?;
insert_activity(&conn, self.creator_id, &undo, true)?; insert_activity(self.creator_id, undo.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // But for delete, the creator is the actor, and does the signing
send_activity(&undo, creator, self.get_follower_inboxes(&conn)?)?; send_activity(client, &undo, creator, inboxes).await?;
Ok(()) Ok(())
} }
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
let group = self.to_apub(conn)?; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -228,17 +266,25 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_object_base_box(BaseBox::from_concrete(group)?)?;
insert_activity(&conn, mod_.id, &remove, true)?; insert_activity(mod_.id, remove.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // But for delete, the creator is the actor, and does the signing
send_activity(&remove, mod_, self.get_follower_inboxes(&conn)?)?; send_activity(client, &remove, mod_, inboxes).await?;
Ok(()) Ok(())
} }
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
let group = self.to_apub(conn)?; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -268,51 +314,69 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(remove)?; .set_object_base_box(remove)?;
insert_activity(&conn, mod_.id, &undo, true)?; insert_activity(mod_.id, undo.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for remove , the creator is the actor, and does the signing // But for remove , the creator is the actor, and does the signing
send_activity(&undo, mod_, self.get_follower_inboxes(&conn)?)?; send_activity(client, &undo, mod_, inboxes).await?;
Ok(()) Ok(())
} }
/// For a given community, returns the inboxes of all followers. /// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> { async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError> {
Ok( let id = self.id;
CommunityFollowerView::for_community(conn, self.id)?
.into_iter() let inboxes = blocking(pool, move |conn| {
.map(|c| get_shared_inbox(&c.user_actor_id)) CommunityFollowerView::for_community(conn, id)
.filter(|s| !s.is_empty()) })
.unique() .await??;
.collect(), let inboxes = inboxes
) .into_iter()
.map(|c| get_shared_inbox(&c.user_actor_id))
.filter(|s| !s.is_empty())
.unique()
.collect();
Ok(inboxes)
} }
fn send_follow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { async fn send_follow(
&self,
_follow_actor_id: &str,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_unfollow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { async fn send_unfollow(
&self,
_follow_actor_id: &str,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for CommunityForm { impl FromApub for CommunityForm {
type ApubType = GroupExt; type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community. /// Parse an ActivityPub group received from another instance into a Lemmy community.
fn from_apub(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> { async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
let group_extensions: &GroupExtension = &group.ext_one; let group_extensions: &GroupExtension = &group.ext_one;
let oprops = &group.inner.object_props; let oprops = &group.inner.object_props;
let aprops = &group.ext_two; let aprops = &group.ext_two;
let public_key: &PublicKey = &group.ext_three.public_key; let public_key: &PublicKey = &group.ext_three.public_key;
let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap(); let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap();
let creator = creator_and_moderator_uris let creator_uri = creator_and_moderator_uris.next().unwrap();
.next()
.map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap()) let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
.unwrap();
Ok(CommunityForm { Ok(CommunityForm {
name: oprops.get_name_xsd_string().unwrap().to_string(), name: oprops.get_name_xsd_string().unwrap().to_string(),
@ -342,14 +406,18 @@ impl FromApub for CommunityForm {
/// Return the community json over HTTP. /// Return the community json over HTTP.
pub async fn get_apub_community_http( pub async fn get_apub_community_http(
info: Path<CommunityQuery>, info: web::Path<CommunityQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let community = Community::read_from_name(&&db.get()?, &info.community_name)?; let community = blocking(&db, move |conn| {
Community::read_from_name(conn, &info.community_name)
})
.await??;
if !community.deleted { if !community.deleted {
Ok(create_apub_response( let apub = community.to_apub(&db).await?;
&community.to_apub(&db.get().unwrap())?,
)) Ok(create_apub_response(&apub))
} else { } else {
Ok(create_apub_tombstone_response(&community.to_tombstone()?)) Ok(create_apub_tombstone_response(&community.to_tombstone()?))
} }
@ -357,15 +425,19 @@ pub async fn get_apub_community_http(
/// Returns an empty followers collection, only populating the size (for privacy). /// Returns an empty followers collection, only populating the size (for privacy).
pub async fn get_apub_community_followers( pub async fn get_apub_community_followers(
info: Path<CommunityQuery>, info: web::Path<CommunityQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let community = Community::read_from_name(&&db.get()?, &info.community_name)?; let community = blocking(&db, move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
let conn = db.get()?; let community_id = community.id;
let community_followers = blocking(&db, move |conn| {
//As we are an object, we validated that the community id was valid CommunityFollowerView::for_community(&conn, community_id)
let community_followers = CommunityFollowerView::for_community(&conn, community.id).unwrap(); })
.await??;
let mut collection = UnorderedCollection::default(); let mut collection = UnorderedCollection::default();
let oprops: &mut ObjectProperties = collection.as_mut(); let oprops: &mut ObjectProperties = collection.as_mut();
@ -379,12 +451,13 @@ pub async fn get_apub_community_followers(
} }
impl Community { impl Community {
pub fn do_announce<A>( pub async fn do_announce<A>(
activity: A, activity: A,
community: &Community, community: &Community,
sender: &dyn ActorType, sender: &dyn ActorType,
conn: &PgConnection, client: &Client,
) -> Result<HttpResponse, Error> pool: &DbPool,
) -> Result<HttpResponse, LemmyError>
where where
A: Activity + Base + Serialize + Debug, A: Activity + Base + Serialize + Debug,
{ {
@ -399,15 +472,16 @@ impl Community {
.set_actor_xsd_any_uri(community.actor_id.to_owned())? .set_actor_xsd_any_uri(community.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(activity)?)?; .set_object_base_box(BaseBox::from_concrete(activity)?)?;
insert_activity(&conn, community.creator_id, &announce, true)?; insert_activity(community.creator_id, announce.clone(), true, pool).await?;
// dont send to the instance where the activity originally came from, because that would result // dont send to the instance where the activity originally came from, because that would result
// in a database error (same data inserted twice) // in a database error (same data inserted twice)
let mut to = community.get_follower_inboxes(&conn)?; let mut to = community.get_follower_inboxes(pool).await?;
// this seems to be the "easiest" stable alternative for remove_item() // this seems to be the "easiest" stable alternative for remove_item()
to.retain(|x| *x != sender.get_shared_inbox_url()); to.retain(|x| *x != sender.get_shared_inbox_url());
send_activity(&announce, community, to)?; send_activity(client, &announce, community, to).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }

View file

@ -4,6 +4,7 @@ use crate::{
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
ActorType, ActorType,
}, },
blocking,
db::{ db::{
activity::insert_activity, activity::insert_activity,
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower, CommunityFollowerForm},
@ -11,14 +12,14 @@ use crate::{
Followable, Followable,
}, },
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
LemmyError,
}; };
use activitystreams::activity::Undo; use activitystreams::activity::Undo;
use activitystreams_new::activity::Follow; use activitystreams_new::activity::Follow;
use actix_web::{web, HttpRequest, HttpResponse, Result}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug;
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -28,7 +29,7 @@ pub enum CommunityAcceptedObjects {
} }
impl CommunityAcceptedObjects { impl CommunityAcceptedObjects {
fn follow(&self) -> Result<Follow, Error> { fn follow(&self) -> Result<Follow, LemmyError> {
match self { match self {
CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()), CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
CommunityAcceptedObjects::Undo(u) => Ok( CommunityAcceptedObjects::Undo(u) => Ok(
@ -49,16 +50,22 @@ pub async fn community_inbox(
input: web::Json<CommunityAcceptedObjects>, input: web::Json<CommunityAcceptedObjects>,
path: web::Path<String>, path: web::Path<String>,
db: DbPoolParam, db: DbPoolParam,
client: web::Data<Client>,
_chat_server: ChatServerParam, _chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let input = input.into_inner(); let input = input.into_inner();
let conn = db.get()?;
let community = Community::read_from_name(&conn, &path.into_inner())?; let path = path.into_inner();
let community = blocking(&db, move |conn| Community::read_from_name(&conn, &path)).await??;
if !community.local { if !community.local {
return Err(format_err!( return Err(
"Received activity is addressed to remote community {}", format_err!(
&community.actor_id "Received activity is addressed to remote community {}",
)); &community.actor_id
)
.into(),
);
} }
debug!( debug!(
"Community {} received activity {:?}", "Community {} received activity {:?}",
@ -68,28 +75,27 @@ pub async fn community_inbox(
let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string(); let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string();
let conn = db.get()?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, &client, &db).await?;
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &client, &db).await?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
verify(&request, &user)?; verify(&request, &user)?;
match input { match input {
CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &user, &community, &conn), CommunityAcceptedObjects::Follow(f) => handle_follow(f, user, community, &client, db).await,
CommunityAcceptedObjects::Undo(u) => handle_undo_follow(&u, &user, &community, &conn), CommunityAcceptedObjects::Undo(u) => handle_undo_follow(u, user, community, db).await,
} }
} }
/// Handle a follow request from a remote user, adding it to the local database and returning an /// Handle a follow request from a remote user, adding it to the local database and returning an
/// Accept activity. /// Accept activity.
fn handle_follow( async fn handle_follow(
follow: &Follow, follow: Follow,
user: &User_, user: User_,
community: &Community, community: Community,
conn: &PgConnection, client: &Client,
) -> Result<HttpResponse, Error> { db: DbPoolParam,
insert_activity(&conn, user.id, &follow, false)?; ) -> Result<HttpResponse, LemmyError> {
insert_activity(user.id, follow.clone(), false, &db).await?;
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
@ -97,27 +103,34 @@ fn handle_follow(
}; };
// This will fail if they're already a follower, but ignore the error. // This will fail if they're already a follower, but ignore the error.
CommunityFollower::follow(&conn, &community_follower_form).ok(); blocking(&db, move |conn| {
CommunityFollower::follow(&conn, &community_follower_form).ok()
})
.await?;
community.send_accept_follow(&follow, &conn)?; community.send_accept_follow(&follow, &client, &db).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn handle_undo_follow( async fn handle_undo_follow(
undo: &Undo, undo: Undo,
user: &User_, user: User_,
community: &Community, community: Community,
conn: &PgConnection, db: DbPoolParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
insert_activity(&conn, user.id, &undo, false)?; insert_activity(user.id, undo, false, &db).await?;
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
user_id: user.id, user_id: user.id,
}; };
CommunityFollower::unfollow(&conn, &community_follower_form).ok(); // This will fail if they aren't a follower, but ignore the error.
blocking(&db, move |conn| {
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
})
.await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }

View file

@ -1,7 +1,9 @@
use crate::db::{category::Category, Crud}; use crate::{
db::{category::Category, Crud},
LemmyError,
};
use activitystreams::{ext::Extension, Actor}; use activitystreams::{ext::Extension, Actor};
use diesel::PgConnection; use diesel::PgConnection;
use failure::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
@ -24,7 +26,7 @@ impl GroupExtension {
conn: &PgConnection, conn: &PgConnection,
category_id: i32, category_id: i32,
sensitive: bool, sensitive: bool,
) -> Result<GroupExtension, Error> { ) -> Result<GroupExtension, LemmyError> {
let category = Category::read(conn, category_id)?; let category = Category::read(conn, category_id)?;
let group_category = GroupCategory { let group_category = GroupCategory {
identifier: category_id.to_string(), identifier: category_id.to_string(),

View file

@ -1,9 +1,10 @@
use crate::apub::ActorType; use crate::{apub::ActorType, LemmyError};
use activitystreams::ext::Extension; use activitystreams::ext::Extension;
use actix_web::HttpRequest; use actix_web::{client::ClientRequest, HttpRequest};
use failure::Error; use http_signature_normalization_actix::{
use http::request::Builder; digest::{DigestClient, SignExt},
use http_signature_normalization::Config; Config,
};
use log::debug; use log::debug;
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
@ -12,7 +13,7 @@ use openssl::{
sign::{Signer, Verifier}, sign::{Signer, Verifier},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use sha2::{Digest, Sha256};
lazy_static! { lazy_static! {
static ref HTTP_SIG_CONFIG: Config = Config::new(); static ref HTTP_SIG_CONFIG: Config = Config::new();
@ -24,7 +25,7 @@ pub struct Keypair {
} }
/// Generate the asymmetric keypair for ActivityPub HTTP signatures. /// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn generate_actor_keypair() -> Result<Keypair, Error> { pub fn generate_actor_keypair() -> Result<Keypair, LemmyError> {
let rsa = Rsa::generate(2048)?; let rsa = Rsa::generate(2048)?;
let pkey = PKey::from_rsa(rsa)?; let pkey = PKey::from_rsa(rsa)?;
let public_key = pkey.public_key_to_pem()?; let public_key = pkey.public_key_to_pem()?;
@ -36,56 +37,41 @@ pub fn generate_actor_keypair() -> Result<Keypair, Error> {
} }
/// Signs request headers with the given keypair. /// Signs request headers with the given keypair.
pub fn sign(request: &Builder, actor: &dyn ActorType) -> Result<String, Error> { pub async fn sign(
request: ClientRequest,
actor: &dyn ActorType,
activity: String,
) -> Result<DigestClient<String>, LemmyError> {
let signing_key_id = format!("{}#main-key", actor.actor_id()); let signing_key_id = format!("{}#main-key", actor.actor_id());
let private_key = actor.private_key();
let headers = request let digest_client = request
.headers_ref() .signature_with_digest(
.unwrap() HTTP_SIG_CONFIG.clone(),
.iter() signing_key_id,
.map(|h| -> Result<(String, String), Error> { Sha256::new(),
Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned())) activity,
}) move |signing_string| {
.collect::<Result<BTreeMap<String, String>, Error>>()?; let private_key = PKey::private_key_from_pem(private_key.as_bytes())?;
let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
signer.update(signing_string.as_bytes()).unwrap();
let signature_header_value = HTTP_SIG_CONFIG Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, LemmyError>
.begin_sign( },
request.method_ref().unwrap().as_str(), )
request .await?;
.uri_ref()
.unwrap()
.path_and_query()
.unwrap()
.as_str(),
headers,
)?
.sign(signing_key_id, |signing_string| {
let private_key = PKey::private_key_from_pem(actor.private_key().as_bytes())?;
let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
signer.update(signing_string.as_bytes()).unwrap();
Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, Error>
})?
.signature_header();
Ok(signature_header_value) Ok(digest_client)
} }
pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error> { pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> {
let headers = request
.headers()
.iter()
.map(|h| -> Result<(String, String), Error> {
Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned()))
})
.collect::<Result<BTreeMap<String, String>, Error>>()?;
let verified = HTTP_SIG_CONFIG let verified = HTTP_SIG_CONFIG
.begin_verify( .begin_verify(
request.method().as_str(), request.method(),
request.uri().path_and_query().unwrap().as_str(), request.uri().path_and_query(),
headers, request.headers().clone(),
)? )?
.verify(|signature, signing_string| -> Result<bool, Error> { .verify(|signature, signing_string| -> Result<bool, LemmyError> {
debug!( debug!(
"Verifying with key {}, message {}", "Verifying with key {}, message {}",
&actor.public_key(), &actor.public_key(),
@ -101,10 +87,7 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error>
debug!("verified signature for {}", &request.uri()); debug!("verified signature for {}", &request.uri());
Ok(()) Ok(())
} else { } else {
Err(format_err!( Err(format_err!("Invalid signature on request: {}", &request.uri()).into())
"Invalid signature on request: {}",
&request.uri()
))
} }
} }

View file

@ -1,15 +1,14 @@
use activitystreams::object::Note; use activitystreams::object::Note;
use actix_web::Result; use actix_web::client::Client;
use diesel::{result::Error::NotFound, PgConnection}; use diesel::{result::Error::NotFound, PgConnection};
use failure::{Error, _core::fmt::Debug};
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::time::Duration; use std::{fmt::Debug, time::Duration};
use url::Url; use url::Url;
use crate::{ use crate::{
api::site::SearchResponse, api::site::SearchResponse,
blocking,
db::{ db::{
comment::{Comment, CommentForm}, comment::{Comment, CommentForm},
comment_view::CommentView, comment_view::CommentView,
@ -23,7 +22,10 @@ use crate::{
SearchType, SearchType,
}, },
naive_now, naive_now,
request::{retry, RecvError},
routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}, routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
DbPool,
LemmyError,
}; };
use crate::{ use crate::{
@ -43,36 +45,50 @@ use chrono::NaiveDateTime;
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60; static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
// Fetch nodeinfo metadata from a remote instance. // Fetch nodeinfo metadata from a remote instance.
fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> { async fn _fetch_node_info(client: &Client, domain: &str) -> Result<NodeInfo, LemmyError> {
let well_known_uri = Url::parse(&format!( let well_known_uri = Url::parse(&format!(
"{}://{}/.well-known/nodeinfo", "{}://{}/.well-known/nodeinfo",
get_apub_protocol_string(), get_apub_protocol_string(),
domain domain
))?; ))?;
let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?) let well_known = fetch_remote_object::<NodeInfoWellKnown>(client, &well_known_uri).await?;
let nodeinfo = fetch_remote_object::<NodeInfo>(client, &well_known.links.href).await?;
Ok(nodeinfo)
} }
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation, /// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc. /// timeouts etc.
pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error> pub async fn fetch_remote_object<Response>(
client: &Client,
url: &Url,
) -> Result<Response, LemmyError>
where where
Response: for<'de> Deserialize<'de>, Response: for<'de> Deserialize<'de>,
{ {
if !is_apub_id_valid(&url) { if !is_apub_id_valid(&url) {
return Err(format_err!("Activitypub uri invalid or blocked: {}", url)); return Err(format_err!("Activitypub uri invalid or blocked: {}", url).into());
} }
// TODO: this function should return a future
let timeout = Duration::from_secs(60); let timeout = Duration::from_secs(60);
let text = Request::get(url.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE) let json = retry(|| {
.connect_timeout(timeout) client
.timeout(timeout) .get(url.as_str())
.body(())? .header("Accept", APUB_JSON_CONTENT_TYPE)
.send()? .timeout(timeout)
.text()?; .send()
let res: Response = serde_json::from_str(&text)?; })
Ok(res) .await?
.json()
.await
.map_err(|e| {
debug!("Receive error, {}", e);
RecvError(e.to_string())
})?;
Ok(json)
} }
/// The types of ActivityPub objects that can be fetched directly by searching for their ID. /// The types of ActivityPub objects that can be fetched directly by searching for their ID.
@ -92,7 +108,11 @@ pub enum SearchAcceptedObjects {
/// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540 /// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540
/// http://lemmy_alpha:8540/post/3 /// http://lemmy_alpha:8540/post/3
/// http://lemmy_alpha:8540/comment/2 /// http://lemmy_alpha:8540/comment/2
pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> { pub async fn search_by_apub_id(
query: &str,
client: &Client,
pool: &DbPool,
) -> Result<SearchResponse, LemmyError> {
// Parse the shorthand query url // Parse the shorthand query url
let query_url = if query.contains('@') { let query_url = if query.contains('@') {
debug!("{}", query); debug!("{}", query);
@ -107,10 +127,10 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
let split2 = split[0].split('!').collect::<Vec<&str>>(); let split2 = split[0].split('!').collect::<Vec<&str>>();
(format!("/c/{}", split2[1]), split[1]) (format!("/c/{}", split2[1]), split[1])
} else { } else {
return Err(format_err!("Invalid search query: {}", query)); return Err(format_err!("Invalid search query: {}", query).into());
} }
} else { } else {
return Err(format_err!("Invalid search query: {}", query)); return Err(format_err!("Invalid search query: {}", query).into());
}; };
let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name); let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
@ -126,22 +146,41 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
communities: vec![], communities: vec![],
users: vec![], users: vec![],
}; };
match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? {
SearchAcceptedObjects::Person(p) => { SearchAcceptedObjects::Person(p) => {
let user_uri = p.inner.object_props.get_id().unwrap().to_string(); let user_uri = p.inner.object_props.get_id().unwrap().to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
response.users = vec![UserView::read(conn, user.id)?]; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
response.users = vec![blocking(pool, move |conn| UserView::read(conn, user.id)).await??];
response
} }
SearchAcceptedObjects::Group(g) => { SearchAcceptedObjects::Group(g) => {
let community_uri = g.inner.object_props.get_id().unwrap().to_string(); let community_uri = g.inner.object_props.get_id().unwrap().to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
let community =
get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
// TODO Maybe at some point in the future, fetch all the history of a community // TODO Maybe at some point in the future, fetch all the history of a community
// fetch_community_outbox(&c, conn)?; // fetch_community_outbox(&c, conn)?;
response.communities = vec![CommunityView::read(conn, community.id, None)?]; response.communities = vec![
blocking(pool, move |conn| {
CommunityView::read(conn, community.id, None)
})
.await??,
];
response
} }
SearchAcceptedObjects::Page(p) => { SearchAcceptedObjects::Page(p) => {
let p = upsert_post(&PostForm::from_apub(&p, conn)?, conn)?; let post_form = PostForm::from_apub(&p, client, pool).await?;
response.posts = vec![PostView::read(conn, p.id, None)?];
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(c) => { SearchAcceptedObjects::Comment(c) => {
let post_url = c let post_url = c
@ -151,41 +190,59 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
.next() .next()
.unwrap() .unwrap()
.to_string(); .to_string();
// TODO: also fetch parent comments if any // TODO: also fetch parent comments if any
let post = fetch_remote_object(&Url::parse(&post_url)?)?; let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
upsert_post(&PostForm::from_apub(&post, conn)?, conn)?; let post_form = PostForm::from_apub(&post, client, pool).await?;
let c = upsert_comment(&CommentForm::from_apub(&c, conn)?, conn)?; let comment_form = CommentForm::from_apub(&c, client, pool).await?;
response.comments = vec![CommentView::read(conn, c.id, None)?];
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
response.comments =
vec![blocking(pool, move |conn| CommentView::read(conn, c.id, None)).await??];
response
} }
} };
Ok(response) Ok(response)
} }
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user. /// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
pub fn get_or_fetch_and_upsert_remote_user( pub async fn get_or_fetch_and_upsert_remote_user(
apub_id: &str, apub_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<User_, Error> { pool: &DbPool,
match User_::read_from_actor_id(&conn, &apub_id) { ) -> Result<User_, LemmyError> {
Ok(u) => { let apub_id_owned = apub_id.to_owned();
// If its older than a day, re-fetch it let user = blocking(pool, move |conn| {
if !u.local && should_refetch_actor(u.last_refreshed_at) { User_::read_from_actor_id(conn, &apub_id_owned)
debug!("Fetching and updating from remote user: {}", apub_id); })
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?; .await?;
let mut uf = UserForm::from_apub(&person, &conn)?;
uf.last_refreshed_at = Some(naive_now()); match user {
Ok(User_::update(&conn, u.id, &uf)?) // If its older than a day, re-fetch it
} else { Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
Ok(u) debug!("Fetching and updating from remote user: {}", apub_id);
} let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).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(user)
} }
Ok(u) => Ok(u),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id); debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?; let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
let uf = UserForm::from_apub(&person, &conn)?;
Ok(User_::create(conn, &uf)?) let uf = UserForm::from_apub(&person, client, pool).await?;
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
Ok(user)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
@ -204,27 +261,35 @@ fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
} }
/// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community. /// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
pub fn get_or_fetch_and_upsert_remote_community( pub async fn get_or_fetch_and_upsert_remote_community(
apub_id: &str, apub_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<Community, Error> { pool: &DbPool,
match Community::read_from_actor_id(&conn, &apub_id) { ) -> Result<Community, LemmyError> {
Ok(c) => { let apub_id_owned = apub_id.to_owned();
if !c.local && should_refetch_actor(c.last_refreshed_at) { let community = blocking(pool, move |conn| {
debug!("Fetching and updating from remote community: {}", apub_id); Community::read_from_actor_id(conn, &apub_id_owned)
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?; })
let mut cf = CommunityForm::from_apub(&group, conn)?; .await?;
cf.last_refreshed_at = Some(naive_now());
Ok(Community::update(&conn, c.id, &cf)?) match community {
} else { Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
Ok(c) debug!("Fetching and updating from remote community: {}", apub_id);
} let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).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(community)
} }
Ok(c) => Ok(c),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id); debug!("Fetching and creating remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?; let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
let cf = CommunityForm::from_apub(&group, conn)?;
let community = Community::create(conn, &cf)?; 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 // Also add the community moderators too
let creator_and_moderator_uris = group let creator_and_moderator_uris = group
@ -232,74 +297,105 @@ pub fn get_or_fetch_and_upsert_remote_community(
.object_props .object_props
.get_many_attributed_to_xsd_any_uris() .get_many_attributed_to_xsd_any_uris()
.unwrap(); .unwrap();
let creator_and_moderators = creator_and_moderator_uris
.map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
.collect::<Vec<User_>>();
for mod_ in creator_and_moderators { let mut creator_and_moderators = Vec::new();
let community_moderator_form = CommunityModeratorForm {
community_id: community.id, for uri in creator_and_moderator_uris {
user_id: mod_.id, let c_or_m = get_or_fetch_and_upsert_remote_user(uri.as_str(), client, pool).await?;
};
CommunityModerator::join(&conn, &community_moderator_form)?; creator_and_moderators.push(c_or_m);
} }
let community_id = community.id;
blocking(pool, move |conn| {
for mod_ in creator_and_moderators {
let community_moderator_form = CommunityModeratorForm {
community_id,
user_id: mod_.id,
};
CommunityModerator::join(conn, &community_moderator_form)?;
}
Ok(()) as Result<(), LemmyError>
})
.await??;
Ok(community) Ok(community)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> { fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, LemmyError> {
let existing = Post::read_from_apub_id(conn, &post_form.ap_id); let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
match existing { match existing {
Err(NotFound {}) => Ok(Post::create(conn, &post_form)?), Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
Ok(p) => Ok(Post::update(conn, p.id, &post_form)?), Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
pub fn get_or_fetch_and_insert_remote_post( pub async fn get_or_fetch_and_insert_remote_post(
post_ap_id: &str, post_ap_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<Post, Error> { pool: &DbPool,
match Post::read_from_apub_id(conn, post_ap_id) { ) -> Result<Post, LemmyError> {
let post_ap_id_owned = post_ap_id.to_owned();
let post = blocking(pool, move |conn| {
Post::read_from_apub_id(conn, &post_ap_id_owned)
})
.await?;
match post {
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id); debug!("Fetching and creating remote post: {}", post_ap_id);
let post = fetch_remote_object::<PageExt>(&Url::parse(post_ap_id)?)?; let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
let post_form = PostForm::from_apub(&post, conn)?; let post_form = PostForm::from_apub(&post, client, pool).await?;
Ok(Post::create(conn, &post_form)?)
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
Ok(post)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, Error> { fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, LemmyError> {
let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id); let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
match existing { match existing {
Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?), Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?),
Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?), Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?),
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
pub fn get_or_fetch_and_insert_remote_comment( pub async fn get_or_fetch_and_insert_remote_comment(
comment_ap_id: &str, comment_ap_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<Comment, Error> { pool: &DbPool,
match Comment::read_from_apub_id(conn, comment_ap_id) { ) -> Result<Comment, LemmyError> {
let comment_ap_id_owned = comment_ap_id.to_owned();
let comment = blocking(pool, move |conn| {
Comment::read_from_apub_id(conn, &comment_ap_id_owned)
})
.await?;
match comment {
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!( debug!(
"Fetching and creating remote comment and its parents: {}", "Fetching and creating remote comment and its parents: {}",
comment_ap_id comment_ap_id
); );
let comment = fetch_remote_object::<Note>(&Url::parse(comment_ap_id)?)?; let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
let comment_form = CommentForm::from_apub(&comment, conn)?; let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
Ok(Comment::create(conn, &comment_form)?)
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;
Ok(comment)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
@ -309,7 +405,7 @@ pub fn get_or_fetch_and_insert_remote_comment(
// maybe), is community and user actors // maybe), is community and user actors
// and user actors // and user actors
// Fetch all posts in the outbox of the given user, and insert them into the database. // Fetch all posts in the outbox of the given user, and insert them into the database.
// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, Error> { // fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, LemmyError> {
// let outbox_url = Url::parse(&community.get_outbox_url())?; // let outbox_url = Url::parse(&community.get_outbox_url())?;
// let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?; // let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
// let items = outbox.collection_props.get_many_items_base_boxes(); // let items = outbox.collection_props.get_many_items_base_boxes();
@ -317,11 +413,11 @@ pub fn get_or_fetch_and_insert_remote_comment(
// Ok( // Ok(
// items // items
// .unwrap() // .unwrap()
// .map(|obox: &BaseBox| -> Result<PostForm, Error> { // .map(|obox: &BaseBox| -> Result<PostForm, LemmyError> {
// let page = obox.clone().to_concrete::<Page>()?; // let page = obox.clone().to_concrete::<Page>()?;
// PostForm::from_page(&page, conn) // PostForm::from_page(&page, conn)
// }) // })
// .map(|pf| upsert_post(&pf?, conn)) // .map(|pf| upsert_post(&pf?, conn))
// .collect::<Result<Vec<Post>, Error>>()?, // .collect::<Result<Vec<Post>, LemmyError>>()?,
// ) // )
// } // }

View file

@ -18,7 +18,10 @@ use crate::{
}, },
convert_datetime, convert_datetime,
db::user::User_, db::user::User_,
request::{retry, RecvError},
routes::webfinger::WebFingerResponse, routes::webfinger::WebFingerResponse,
DbPool,
LemmyError,
MentionData, MentionData,
Settings, Settings,
}; };
@ -28,11 +31,8 @@ use activitystreams::{
}; };
use activitystreams_ext::{Ext1, Ext2, Ext3}; use activitystreams_ext::{Ext1, Ext2, Ext3};
use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*}; use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*};
use actix_web::{body::Body, HttpResponse, Result}; use actix_web::{body::Body, client::Client, HttpResponse};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::PgConnection;
use failure::Error;
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
@ -101,7 +101,9 @@ pub fn get_apub_protocol_string() -> &'static str {
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list. // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
fn is_apub_id_valid(apub_id: &Url) -> bool { fn is_apub_id_valid(apub_id: &Url) -> bool {
debug!("Checking {}", apub_id);
if apub_id.scheme() != get_apub_protocol_string() { if apub_id.scheme() != get_apub_protocol_string() {
debug!("invalid scheme: {:?}", apub_id.scheme());
return false; return false;
} }
@ -112,15 +114,27 @@ fn is_apub_id_valid(apub_id: &Url) -> bool {
.map(|d| d.to_string()) .map(|d| d.to_string())
.collect(); .collect();
match apub_id.domain() { match apub_id.domain() {
Some(d) => allowed_instances.contains(&d.to_owned()), Some(d) => {
None => false, let contains = allowed_instances.contains(&d.to_owned());
if !contains {
debug!("{} not in {:?}", d, allowed_instances);
}
contains
}
None => {
debug!("missing domain");
false
}
} }
} }
#[async_trait::async_trait(?Send)]
pub trait ToApub { pub trait ToApub {
type Response; type Response;
fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>; async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>;
fn to_tombstone(&self) -> Result<Tombstone, Error>; fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
} }
/// Updated is actually the deletion time /// Updated is actually the deletion time
@ -129,7 +143,7 @@ fn create_tombstone(
object_id: &str, object_id: &str,
updated: Option<NaiveDateTime>, updated: Option<NaiveDateTime>,
former_type: String, former_type: String,
) -> Result<Tombstone, Error> { ) -> Result<Tombstone, LemmyError> {
if deleted { if deleted {
if let Some(updated) = updated { if let Some(updated) = updated {
let mut tombstone = Tombstone::new(); let mut tombstone = Tombstone::new();
@ -138,37 +152,85 @@ fn create_tombstone(
tombstone.set_deleted(convert_datetime(updated).into()); tombstone.set_deleted(convert_datetime(updated).into());
Ok(tombstone) Ok(tombstone)
} else { } else {
Err(format_err!( Err(format_err!("Cant convert to tombstone because updated time was None.").into())
"Cant convert to tombstone because updated time was None."
))
} }
} else { } else {
Err(format_err!( Err(format_err!("Cant convert object to tombstone if it wasnt deleted").into())
"Cant convert object to tombstone if it wasnt deleted"
))
} }
} }
#[async_trait::async_trait(?Send)]
pub trait FromApub { pub trait FromApub {
type ApubType; type ApubType;
fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error> async fn from_apub(
apub: &Self::ApubType,
client: &Client,
pool: &DbPool,
) -> Result<Self, LemmyError>
where where
Self: Sized; Self: Sized;
} }
#[async_trait::async_trait(?Send)]
pub trait ApubObjectType { pub trait ApubObjectType {
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_create(
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; creator: &User_,
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; client: &Client,
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; pool: &DbPool,
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; ) -> Result<(), LemmyError>;
async fn send_update(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
} }
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType { pub trait ApubLikeableType {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_like(
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_dislike(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_like(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
} }
pub fn get_shared_inbox(actor_id: &str) -> String { pub fn get_shared_inbox(actor_id: &str) -> String {
@ -185,6 +247,7 @@ pub fn get_shared_inbox(actor_id: &str) -> String {
) )
} }
#[async_trait::async_trait(?Send)]
pub trait ActorType { pub trait ActorType {
fn actor_id(&self) -> String; fn actor_id(&self) -> String;
@ -194,20 +257,55 @@ pub trait ActorType {
// These two have default impls, since currently a community can't follow anything, // These two have default impls, since currently a community can't follow anything,
// and a user can't be followed (yet) // and a user can't be followed (yet)
#[allow(unused_variables)] #[allow(unused_variables)]
fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; async fn send_follow(
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; &self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_unfollow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
#[allow(unused_variables)] #[allow(unused_variables)]
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>; async fn send_accept_follow(
&self,
follow: &Follow,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_delete(
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_remove(
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
/// For a given community, returns the inboxes of all followers. /// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error>; async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>;
// TODO move these to the db rows // TODO move these to the db rows
fn get_inbox_url(&self) -> String { fn get_inbox_url(&self) -> String {
@ -244,7 +342,10 @@ pub trait ActorType {
} }
} }
pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> { pub async fn fetch_webfinger_url(
mention: &MentionData,
client: &Client,
) -> Result<String, LemmyError> {
let fetch_url = format!( let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}", "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
get_apub_protocol_string(), get_apub_protocol_string(),
@ -253,8 +354,14 @@ pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
mention.domain mention.domain
); );
debug!("Fetching webfinger url: {}", &fetch_url); debug!("Fetching webfinger url: {}", &fetch_url);
let text = isahc::get(&fetch_url)?.text()?;
let res: WebFingerResponse = serde_json::from_str(&text)?; let mut response = retry(|| client.get(&fetch_url).send()).await?;
let res: WebFingerResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
let link = res let link = res
.links .links
.iter() .iter()
@ -263,5 +370,5 @@ pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
link link
.href .href
.to_owned() .to_owned()
.ok_or_else(|| format_err!("No href found.")) .ok_or_else(|| format_err!("No href found.").into())
} }

View file

@ -14,6 +14,7 @@ use crate::{
PageExt, PageExt,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
community::Community, community::Community,
@ -22,6 +23,8 @@ use crate::{
Crud, Crud,
}, },
routes::DbPoolParam, routes::DbPoolParam,
DbPool,
LemmyError,
Settings, Settings,
}; };
use activitystreams::{ use activitystreams::{
@ -32,9 +35,7 @@ use activitystreams::{
}; };
use activitystreams_ext::Ext1; use activitystreams_ext::Ext1;
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -44,27 +45,33 @@ pub struct PostQuery {
/// Return the post json over HTTP. /// Return the post json over HTTP.
pub async fn get_apub_post( pub async fn get_apub_post(
info: Path<PostQuery>, info: web::Path<PostQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let id = info.post_id.parse::<i32>()?; let id = info.post_id.parse::<i32>()?;
let post = Post::read(&&db.get()?, id)?; let post = blocking(&db, move |conn| Post::read(conn, id)).await??;
if !post.deleted { if !post.deleted {
Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?)) Ok(create_apub_response(&post.to_apub(&db).await?))
} else { } else {
Ok(create_apub_tombstone_response(&post.to_tombstone()?)) Ok(create_apub_tombstone_response(&post.to_tombstone()?))
} }
} }
#[async_trait::async_trait(?Send)]
impl ToApub for Post { impl ToApub for Post {
type Response = PageExt; type Response = PageExt;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network. // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
fn to_apub(&self, conn: &PgConnection) -> Result<PageExt, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
let mut page = Page::default(); let mut page = Page::default();
let oprops: &mut ObjectProperties = page.as_mut(); let oprops: &mut ObjectProperties = page.as_mut();
let creator = User_::read(conn, self.creator_id)?;
let community = Community::read(conn, self.community_id)?; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
oprops oprops
// Not needed when the Post is embedded in a collection (like for community outbox) // Not needed when the Post is embedded in a collection (like for community outbox)
@ -141,7 +148,7 @@ impl ToApub for Post {
Ok(Ext1::new(page, ext)) Ok(Ext1::new(page, ext))
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
@ -151,17 +158,26 @@ impl ToApub for Post {
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for PostForm { impl FromApub for PostForm {
type ApubType = PageExt; type ApubType = PageExt;
/// Parse an ActivityPub page received from another instance into a Lemmy post. /// Parse an ActivityPub page received from another instance into a Lemmy post.
fn from_apub(page: &PageExt, conn: &PgConnection) -> Result<PostForm, Error> { async fn from_apub(
page: &PageExt,
client: &Client,
pool: &DbPool,
) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one; let ext = &page.ext_one;
let oprops = &page.inner.object_props; let oprops = &page.inner.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string(); let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_actor_id, &conn)?;
let community =
get_or_fetch_and_upsert_remote_community(&community_actor_id, client, pool).await?;
let thumbnail_url = match oprops.get_image_any_image() { let thumbnail_url = match oprops.get_image_any_image() {
Some(any_image) => any_image Some(any_image) => any_image
@ -221,11 +237,20 @@ impl FromApub for PostForm {
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post { impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community. /// Send out information about a newly created post, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_create(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let mut create = Create::new(); let mut create = Create::new();
@ -241,18 +266,28 @@ impl ApubObjectType for Post {
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
create, create,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_update(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let mut update = Update::new(); let mut update = Update::new();
@ -268,17 +303,27 @@ impl ApubObjectType for Post {
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
update, update,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -293,21 +338,29 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_object_base_box(BaseBox::from_concrete(page)?)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
delete, delete,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -338,20 +391,29 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?; .set_object_base_box(delete)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -366,20 +428,29 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_object_base_box(BaseBox::from_concrete(page)?)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
mod_, mod_,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
remove, remove,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?; async fn send_undo_remove(
let community = Community::read(conn, self.community_id)?; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -409,22 +480,32 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(remove)?; .set_object_base_box(remove)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
mod_, mod_,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Post { impl ApubLikeableType for Post {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_like(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -440,17 +521,27 @@ impl ApubLikeableType for Post {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
like, like,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_dislike(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new(); let mut dislike = Dislike::new();
@ -466,17 +557,27 @@ impl ApubLikeableType for Post {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
dislike, dislike,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_like(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -508,11 +609,13 @@ impl ApubLikeableType for Post {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }

View file

@ -7,6 +7,7 @@ use crate::{
FromApub, FromApub,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
activity::insert_activity, activity::insert_activity,
@ -14,6 +15,8 @@ use crate::{
user::User_, user::User_,
Crud, Crud,
}, },
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Undo, Update}, activity::{Create, Delete, Undo, Update},
@ -21,18 +24,21 @@ use activitystreams::{
object::{kind::NoteType, properties::ObjectProperties, Note}, object::{kind::NoteType, properties::ObjectProperties, Note},
}; };
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::Result; use actix_web::client::Client;
use diesel::PgConnection;
use failure::Error;
#[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage { impl ToApub for PrivateMessage {
type Response = Note; type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut private_message = Note::default(); let mut private_message = Note::default();
let oprops: &mut ObjectProperties = private_message.as_mut(); let oprops: &mut ObjectProperties = private_message.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let recipient = User_::read(&conn, self.recipient_id)?; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
oprops oprops
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
@ -49,7 +55,7 @@ impl ToApub for PrivateMessage {
Ok(private_message) Ok(private_message)
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
@ -59,16 +65,24 @@ impl ToApub for PrivateMessage {
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for PrivateMessageForm { impl FromApub for PrivateMessageForm {
type ApubType = Note; type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy Private message /// Parse an ActivityPub note received from another instance into a Lemmy Private message
fn from_apub(note: &Note, conn: &PgConnection) -> Result<PrivateMessageForm, Error> { async fn from_apub(
note: &Note,
client: &Client,
pool: &DbPool,
) -> Result<PrivateMessageForm, LemmyError> {
let oprops = &note.object_props; let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string(); let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, &conn)?;
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, client, pool).await?;
Ok(PrivateMessageForm { Ok(PrivateMessageForm {
creator_id: creator.id, creator_id: creator.id,
@ -91,12 +105,20 @@ impl FromApub for PrivateMessageForm {
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubObjectType for PrivateMessage { impl ApubObjectType for PrivateMessage {
/// Send out information about a newly created private message /// Send out information about a newly created private message
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_create(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut create = Create::new(); let mut create = Create::new();
create create
@ -110,17 +132,24 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
insert_activity(&conn, creator.id, &create, true)?; insert_activity(creator.id, create.clone(), true, pool).await?;
send_activity(&create, creator, vec![to])?; send_activity(client, &create, creator, vec![to]).await?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_update(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut update = Update::new(); let mut update = Update::new();
update update
@ -134,16 +163,23 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
insert_activity(&conn, creator.id, &update, true)?; insert_activity(creator.id, update.clone(), true, pool).await?;
send_activity(&update, creator, vec![to])?; send_activity(client, &update, creator, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut delete = Delete::new(); let mut delete = Delete::new();
delete delete
@ -157,16 +193,23 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
insert_activity(&conn, creator.id, &delete, true)?; insert_activity(creator.id, delete.clone(), true, pool).await?;
send_activity(&delete, creator, vec![to])?; send_activity(client, &delete, creator, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut delete = Delete::new(); let mut delete = Delete::new();
delete delete
@ -195,17 +238,27 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?; .set_object_base_box(delete)?;
insert_activity(&conn, creator.id, &undo, true)?; insert_activity(creator.id, undo.clone(), true, pool).await?;
send_activity(&undo, creator, vec![to])?; send_activity(client, &undo, creator, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
&self,
_mod_: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_undo_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
&self,
_mod_: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ use crate::{
PersonExt, PersonExt,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
activity::insert_activity, activity::insert_activity,
@ -15,6 +16,8 @@ use crate::{
}, },
naive_now, naive_now,
routes::DbPoolParam, routes::DbPoolParam,
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
actor::{properties::ApActorProperties, Person}, actor::{properties::ApActorProperties, Person},
@ -29,9 +32,7 @@ use activitystreams_new::{
object::Tombstone, object::Tombstone,
prelude::*, prelude::*,
}; };
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -39,11 +40,12 @@ pub struct UserQuery {
user_name: String, user_name: String,
} }
#[async_trait::async_trait(?Send)]
impl ToApub for User_ { impl ToApub for User_ {
type Response = PersonExt; type Response = PersonExt;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn to_apub(&self, _conn: &PgConnection) -> Result<PersonExt, Error> { async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
// TODO go through all these to_string and to_owned() // TODO go through all these to_string and to_owned()
let mut person = Person::default(); let mut person = Person::default();
let oprops: &mut ObjectProperties = person.as_mut(); let oprops: &mut ObjectProperties = person.as_mut();
@ -86,11 +88,12 @@ impl ToApub for User_ {
Ok(Ext2::new(person, actor_props, self.get_public_key_ext())) Ok(Ext2::new(person, actor_props, self.get_public_key_ext()))
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
unimplemented!() unimplemented!()
} }
} }
#[async_trait::async_trait(?Send)]
impl ActorType for User_ { impl ActorType for User_ {
fn actor_id(&self) -> String { fn actor_id(&self) -> String {
self.actor_id.to_owned() self.actor_id.to_owned()
@ -105,19 +108,29 @@ impl ActorType for User_ {
} }
/// As a given local user, send out a follow request to a remote community. /// As a given local user, send out a follow request to a remote community.
fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> { async fn send_follow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id); let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
follow.set_context(context()).set_id(id.parse()?); follow.set_context(context()).set_id(id.parse()?);
let to = format!("{}/inbox", follow_actor_id); let to = format!("{}/inbox", follow_actor_id);
insert_activity(&conn, self.id, &follow, true)?; insert_activity(self.id, follow.clone(), true, pool).await?;
send_activity(&follow, self, vec![to])?; send_activity(client, &follow, self, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> { async fn send_unfollow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id); let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
follow.set_context(context()).set_id(id.parse()?); follow.set_context(context()).set_id(id.parse()?);
@ -130,41 +143,67 @@ impl ActorType for User_ {
let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?); let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?);
undo.set_context(context()).set_id(undo_id.parse()?); undo.set_context(context()).set_id(undo_id.parse()?);
insert_activity(&conn, self.id, &undo, true)?; insert_activity(self.id, undo.clone(), true, pool).await?;
send_activity(&undo, self, vec![to])?; send_activity(client, &undo, self, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_undo_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_accept_follow(&self, _follow: &Follow, _conn: &PgConnection) -> Result<(), Error> { async fn send_accept_follow(
&self,
_follow: &Follow,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn get_follower_inboxes(&self, _conn: &PgConnection) -> Result<Vec<String>, Error> { async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<String>, LemmyError> {
unimplemented!() unimplemented!()
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for UserForm { impl FromApub for UserForm {
type ApubType = PersonExt; type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user. /// Parse an ActivityPub person received from another instance into a Lemmy user.
fn from_apub(person: &PersonExt, _conn: &PgConnection) -> Result<Self, Error> { async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
let oprops = &person.inner.object_props; let oprops = &person.inner.object_props;
let aprops = &person.ext_one; let aprops = &person.ext_one;
let public_key: &PublicKey = &person.ext_two.public_key; let public_key: &PublicKey = &person.ext_two.public_key;
@ -210,10 +249,14 @@ impl FromApub for UserForm {
/// Return the user json over HTTP. /// Return the user json over HTTP.
pub async fn get_apub_user_http( pub async fn get_apub_user_http(
info: Path<UserQuery>, info: web::Path<UserQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?; let user_name = info.into_inner().user_name;
let u = user.to_apub(&db.get().unwrap())?; let user = blocking(&db, move |conn| {
User_::find_by_email_or_username(conn, &user_name)
})
.await??;
let u = user.to_apub(&db).await?;
Ok(create_apub_response(&u)) Ok(create_apub_response(&u))
} }

View file

@ -5,6 +5,7 @@ use crate::{
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
FromApub, FromApub,
}, },
blocking,
db::{ db::{
activity::insert_activity, activity::insert_activity,
community::{CommunityFollower, CommunityFollowerForm}, community::{CommunityFollower, CommunityFollowerForm},
@ -17,16 +18,17 @@ use crate::{
naive_now, naive_now,
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
websocket::{server::SendUserRoomMessage, UserOperation}, websocket::{server::SendUserRoomMessage, UserOperation},
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, Create, Delete, Undo, Update}, activity::{Accept, Create, Delete, Undo, Update},
object::Note, object::Note,
}; };
use actix_web::{web, HttpRequest, HttpResponse, Result}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug;
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -43,51 +45,53 @@ pub async fn user_inbox(
request: HttpRequest, request: HttpRequest,
input: web::Json<UserAcceptedObjects>, input: web::Json<UserAcceptedObjects>,
path: web::Path<String>, path: web::Path<String>,
client: web::Data<Client>,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
// TODO: would be nice if we could do the signature check here, but we cant access the actor property // TODO: would be nice if we could do the signature check here, but we cant access the actor property
let input = input.into_inner(); let input = input.into_inner();
let conn = &db.get().unwrap();
let username = path.into_inner(); let username = path.into_inner();
debug!("User {} received activity: {:?}", &username, &input); debug!("User {} received activity: {:?}", &username, &input);
match input { match input {
UserAcceptedObjects::Accept(a) => receive_accept(&a, &request, &username, &conn), UserAcceptedObjects::Accept(a) => receive_accept(*a, &request, &username, &client, &db).await,
UserAcceptedObjects::Create(c) => { UserAcceptedObjects::Create(c) => {
receive_create_private_message(&c, &request, &conn, chat_server) receive_create_private_message(*c, &request, &client, &db, chat_server).await
} }
UserAcceptedObjects::Update(u) => { UserAcceptedObjects::Update(u) => {
receive_update_private_message(&u, &request, &conn, chat_server) receive_update_private_message(*u, &request, &client, &db, chat_server).await
} }
UserAcceptedObjects::Delete(d) => { UserAcceptedObjects::Delete(d) => {
receive_delete_private_message(&d, &request, &conn, chat_server) receive_delete_private_message(*d, &request, &client, &db, chat_server).await
} }
UserAcceptedObjects::Undo(u) => { UserAcceptedObjects::Undo(u) => {
receive_undo_delete_private_message(&u, &request, &conn, chat_server) receive_undo_delete_private_message(*u, &request, &client, &db, chat_server).await
} }
} }
} }
/// Handle accepted follows. /// Handle accepted follows.
fn receive_accept( async fn receive_accept(
accept: &Accept, accept: Accept,
request: &HttpRequest, request: &HttpRequest,
username: &str, username: &str,
conn: &PgConnection, client: &Client,
) -> Result<HttpResponse, Error> { pool: &DbPool,
) -> Result<HttpResponse, LemmyError> {
let community_uri = accept let community_uri = accept
.accept_props .accept_props
.get_actor_xsd_any_uri() .get_actor_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_uri, conn)?; let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
verify(request, &community)?; verify(request, &community)?;
let user = User_::read_from_name(&conn, username)?; let username = username.to_owned();
let user = blocking(pool, move |conn| User_::read_from_name(conn, &username)).await??;
insert_activity(&conn, community.creator_id, &accept, false)?; insert_activity(community.creator_id, accept, false, pool).await?;
// Now you need to add this to the community follower // Now you need to add this to the community follower
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
@ -96,18 +100,22 @@ fn receive_accept(
}; };
// This will fail if they're already a follower // This will fail if they're already a follower
CommunityFollower::follow(&conn, &community_follower_form)?; blocking(pool, move |conn| {
CommunityFollower::follow(conn, &community_follower_form)
})
.await??;
// TODO: make sure that we actually requested a follow // TODO: make sure that we actually requested a follow
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_create_private_message( async fn receive_create_private_message(
create: &Create, create: Create,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let note = create let note = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
@ -122,36 +130,44 @@ fn receive_create_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &create, false)?; insert_activity(user.id, create, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?; let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let inserted_private_message = PrivateMessage::create(&conn, &private_message)?;
let message = PrivateMessageView::read(&conn, inserted_private_message.id)?; let inserted_private_message = blocking(pool, move |conn| {
PrivateMessage::create(conn, &private_message)
})
.await??;
let res = PrivateMessageResponse { let message = blocking(pool, move |conn| {
message: message.to_owned(), PrivateMessageView::read(conn, inserted_private_message.id)
}; })
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::CreatePrivateMessage, op: UserOperation::CreatePrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_update_private_message( async fn receive_update_private_message(
update: &Update, update: Update,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let note = update let note = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
@ -166,37 +182,52 @@ fn receive_update_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &update, false)?; insert_activity(user.id, update, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?; let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
PrivateMessage::update(conn, private_message_id, &private_message)?;
let message = PrivateMessageView::read(&conn, private_message_id)?; let private_message_ap_id = private_message_form.ap_id.clone();
let private_message = blocking(pool, move |conn| {
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
})
.await??;
let res = PrivateMessageResponse { let private_message_id = private_message.id;
message: message.to_owned(), blocking(pool, move |conn| {
}; PrivateMessage::update(conn, private_message_id, &private_message_form)
})
.await??;
let private_message_id = private_message.id;
let message = blocking(pool, move |conn| {
PrivateMessageView::read(conn, private_message_id)
})
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage, op: UserOperation::EditPrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_delete_private_message( async fn receive_delete_private_message(
delete: &Delete, delete: Delete,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let note = delete let note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
@ -211,15 +242,21 @@ fn receive_delete_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &delete, false)?; insert_activity(user.id, delete, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_ap_id = private_message_form.ap_id;
let private_message = blocking(pool, move |conn| {
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
})
.await??;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm { let private_message_form = PrivateMessageForm {
content: private_message.content, content: private_message_form.content,
recipient_id: private_message.recipient_id, recipient_id: private_message.recipient_id,
creator_id: private_message.creator_id, creator_id: private_message.creator_id,
deleted: Some(true), deleted: Some(true),
@ -229,30 +266,40 @@ fn receive_delete_private_message(
published: None, published: None,
updated: Some(naive_now()), updated: Some(naive_now()),
}; };
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?; let private_message_id = private_message.id;
blocking(pool, move |conn| {
PrivateMessage::update(conn, private_message_id, &private_message_form)
})
.await??;
let res = PrivateMessageResponse { let private_message_id = private_message.id;
message: message.to_owned(), let message = blocking(pool, move |conn| {
}; PrivateMessageView::read(&conn, private_message_id)
})
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage, op: UserOperation::EditPrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_undo_delete_private_message( async fn receive_undo_delete_private_message(
undo: &Undo, undo: Undo,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let delete = undo let delete = undo
.undo_props .undo_props
.get_object_base_box() .get_object_base_box()
@ -275,13 +322,19 @@ fn receive_undo_delete_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &delete, false)?; insert_activity(user.id, delete, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_ap_id = private_message.ap_id.clone();
let private_message_id = blocking(pool, move |conn| {
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id).map(|pm| pm.id)
})
.await??;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm { let private_message_form = PrivateMessageForm {
content: private_message.content, content: private_message.content,
recipient_id: private_message.recipient_id, recipient_id: private_message.recipient_id,
@ -293,18 +346,25 @@ fn receive_undo_delete_private_message(
published: None, published: None,
updated: Some(naive_now()), updated: Some(naive_now()),
}; };
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?; blocking(pool, move |conn| {
PrivateMessage::update(conn, private_message_id, &private_message_form)
})
.await??;
let res = PrivateMessageResponse { let message = blocking(pool, move |conn| {
message: message.to_owned(), PrivateMessageView::read(&conn, private_message_id)
}; })
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage, op: UserOperation::EditPrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });

View file

@ -1,9 +1,9 @@
use crate::{db::Crud, schema::activity}; use crate::{blocking, db::Crud, schema::activity, DbPool, LemmyError};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use failure::_core::fmt::Debug;
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fmt::Debug;
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "activity"] #[table_name = "activity"]
@ -55,12 +55,28 @@ impl Crud<ActivityForm> for Activity {
} }
} }
pub fn insert_activity<T>( 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>(
conn: &PgConnection, conn: &PgConnection,
user_id: i32, user_id: i32,
data: &T, data: &T,
local: bool, local: bool,
) -> Result<(), failure::Error> ) -> Result<(), LemmyError>
where where
T: Serialize + Debug, T: Serialize + Debug,
{ {

View file

@ -10,21 +10,22 @@ use crate::{
apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType}, apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
db::Crud, db::Crud,
naive_now, naive_now,
LemmyError,
}; };
use diesel::*; use diesel::*;
use failure::Error;
use log::info; use log::info;
pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> { pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
user_updates_2020_04_02(&conn)?; user_updates_2020_04_02(&conn)?;
community_updates_2020_04_02(&conn)?; community_updates_2020_04_02(&conn)?;
post_updates_2020_04_03(&conn)?; post_updates_2020_04_03(&conn)?;
comment_updates_2020_04_03(&conn)?; comment_updates_2020_04_03(&conn)?;
private_message_updates_2020_05_05(&conn)?; private_message_updates_2020_05_05(&conn)?;
Ok(()) Ok(())
} }
fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::user_::dsl::*; use crate::schema::user_::dsl::*;
info!("Running user_updates_2020_04_02"); info!("Running user_updates_2020_04_02");
@ -75,7 +76,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::community::dsl::*; use crate::schema::community::dsl::*;
info!("Running community_updates_2020_04_02"); info!("Running community_updates_2020_04_02");
@ -119,7 +120,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> { fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::post::dsl::*; use crate::schema::post::dsl::*;
info!("Running post_updates_2020_04_03"); info!("Running post_updates_2020_04_03");
@ -143,7 +144,7 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> { fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
info!("Running comment_updates_2020_04_03"); info!("Running comment_updates_2020_04_03");
@ -167,7 +168,7 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), Error> { fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::private_message::dsl::*; use crate::schema::private_message::dsl::*;
info!("Running private_message_updates_2020_05_05"); info!("Running private_message_updates_2020_05_05");

View file

@ -12,7 +12,7 @@ use crate::{
// ) // )
// SELECT * FROM MyTree; // SELECT * FROM MyTree;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[belongs_to(Post)] #[belongs_to(Post)]
#[table_name = "comment"] #[table_name = "comment"]
pub struct Comment { pub struct Comment {

View file

@ -5,7 +5,7 @@ use crate::{
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "community"] #[table_name = "community"]
pub struct Community { pub struct Community {
pub id: i32, pub id: i32,

View file

@ -50,8 +50,8 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
impl PasswordResetRequest { impl PasswordResetRequest {
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> { pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.input(token); hasher.update(token);
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec()); let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.finalize().to_vec());
let form = PasswordResetRequestForm { let form = PasswordResetRequestForm {
user_id: from_user_id, user_id: from_user_id,
@ -62,8 +62,8 @@ impl PasswordResetRequest {
} }
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> { pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.input(token); hasher.update(token);
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec()); let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.finalize().to_vec());
password_reset_request password_reset_request
.filter(token_encrypted.eq(token_hash)) .filter(token_encrypted.eq(token_hash))
.filter(published.gt(now - 1.days())) .filter(published.gt(now - 1.days()))

View file

@ -10,7 +10,7 @@ use diesel::{dsl::*, result::Error, *};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug)] #[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "user_"] #[table_name = "user_"]
pub struct User_ { pub struct User_ {
pub id: i32, pub id: i32,

View file

@ -26,20 +26,39 @@ pub extern crate serde_json;
pub extern crate sha2; pub extern crate sha2;
pub extern crate strum; pub extern crate strum;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
where
F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
T: Send + 'static,
{
let pool = pool.clone();
let res = actix_web::web::block(move || {
let conn = pool.get()?;
let res = (f)(&conn);
Ok(res) as Result<_, LemmyError>
})
.await?;
Ok(res)
}
pub mod api; pub mod api;
pub mod apub; pub mod apub;
pub mod db; pub mod db;
pub mod rate_limit; pub mod rate_limit;
pub mod request;
pub mod routes; pub mod routes;
pub mod schema; pub mod schema;
pub mod settings; pub mod settings;
pub mod version; pub mod version;
pub mod websocket; pub mod websocket;
use crate::settings::Settings; use crate::{
use actix_web::dev::ConnectionInfo; request::{retry, RecvError},
settings::Settings,
};
use actix_web::{client::Client, dev::ConnectionInfo};
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc}; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use isahc::prelude::*;
use itertools::Itertools; use itertools::Itertools;
use lettre::{ use lettre::{
smtp::{ smtp::{
@ -58,12 +77,35 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder};
use serde::Deserialize; use serde::Deserialize;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
pub type ConnectionId = usize; pub type ConnectionId = usize;
pub type PostId = i32; pub type PostId = i32;
pub type CommunityId = i32; pub type CommunityId = i32;
pub type UserId = i32; pub type UserId = i32;
pub type IPAddr = String; pub type IPAddr = String;
#[derive(Debug)]
pub struct LemmyError {
inner: failure::Error,
}
impl std::fmt::Display for LemmyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl actix_web::error::ResponseError for LemmyError {}
impl<T> From<T> for LemmyError
where
T: Into<failure::Error>,
{
fn from(t: T) -> Self {
LemmyError { inner: t.into() }
}
}
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> { pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
DateTime::<Utc>::from_utc(ndt, Utc) DateTime::<Utc>::from_utc(ndt, Utc)
} }
@ -85,8 +127,10 @@ pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test) EMAIL_REGEX.is_match(test)
} }
pub fn is_image_content_type(test: &str) -> Result<(), failure::Error> { pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
if isahc::get(test)? let response = retry(|| client.get(test).send()).await?;
if response
.headers() .headers()
.get("Content-Type") .get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))? .ok_or_else(|| format_err!("No Content-Type header"))?
@ -95,7 +139,7 @@ pub fn is_image_content_type(test: &str) -> Result<(), failure::Error> {
{ {
Ok(()) Ok(())
} else { } else {
Err(format_err!("Not an image type.")) Err(format_err!("Not an image type.").into())
} }
} }
@ -178,10 +222,15 @@ pub struct IframelyResponse {
html: Option<String>, html: Option<String>,
} }
pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> { pub async fn fetch_iframely(client: &Client, url: &str) -> Result<IframelyResponse, LemmyError> {
let fetch_url = format!("http://iframely/oembed?url={}", url); let fetch_url = format!("http://iframely/oembed?url={}", url);
let text = isahc::get(&fetch_url)?.text()?;
let res: IframelyResponse = serde_json::from_str(&text)?; let mut response = retry(|| client.get(&fetch_url).send()).await?;
let res: IframelyResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
Ok(res) Ok(res)
} }
@ -197,23 +246,30 @@ pub struct PictrsFile {
delete_token: String, delete_token: String,
} }
pub fn fetch_pictrs(image_url: &str) -> Result<PictrsResponse, failure::Error> { pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResponse, LemmyError> {
is_image_content_type(image_url)?; is_image_content_type(client, image_url).await?;
let fetch_url = format!( let fetch_url = format!(
"http://pictrs:8080/image/download?url={}", "http://pictrs:8080/image/download?url={}",
utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
); );
let text = isahc::get(&fetch_url)?.text()?;
let res: PictrsResponse = serde_json::from_str(&text)?; let mut response = retry(|| client.get(&fetch_url).send()).await?;
if res.msg == "ok" {
Ok(res) let response: PictrsResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
if response.msg == "ok" {
Ok(response)
} else { } else {
Err(format_err!("{}", &res.msg)) Err(format_err!("{}", &response.msg).into())
} }
} }
fn fetch_iframely_and_pictrs_data( async fn fetch_iframely_and_pictrs_data(
client: &Client,
url: Option<String>, url: Option<String>,
) -> ( ) -> (
Option<String>, Option<String>,
@ -225,7 +281,7 @@ fn fetch_iframely_and_pictrs_data(
Some(url) => { Some(url) => {
// Fetch iframely data // Fetch iframely data
let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) = let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
match fetch_iframely(url) { match fetch_iframely(client, url).await {
Ok(res) => (res.title, res.description, res.thumbnail_url, res.html), Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
Err(e) => { Err(e) => {
error!("iframely err: {}", e); error!("iframely err: {}", e);
@ -235,7 +291,7 @@ fn fetch_iframely_and_pictrs_data(
// Fetch pictrs thumbnail // Fetch pictrs thumbnail
let pictrs_thumbnail = match iframely_thumbnail_url { let pictrs_thumbnail = match iframely_thumbnail_url {
Some(iframely_thumbnail_url) => match fetch_pictrs(&iframely_thumbnail_url) { Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
Ok(res) => Some(res.files[0].file.to_owned()), Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => { Err(e) => {
error!("pictrs err: {}", e); error!("pictrs err: {}", e);
@ -243,7 +299,7 @@ fn fetch_iframely_and_pictrs_data(
} }
}, },
// Try to generate a small thumbnail if iframely is not supported // Try to generate a small thumbnail if iframely is not supported
None => match fetch_pictrs(&url) { None => match fetch_pictrs(client, &url).await {
Ok(res) => Some(res.files[0].file.to_owned()), Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => { Err(e) => {
error!("pictrs err: {}", e); error!("pictrs err: {}", e);
@ -269,7 +325,7 @@ pub fn markdown_to_html(text: &str) -> String {
pub fn get_ip(conn_info: &ConnectionInfo) -> String { pub fn get_ip(conn_info: &ConnectionInfo) -> String {
conn_info conn_info
.remote() .remote_addr()
.unwrap_or("127.0.0.1:12345") .unwrap_or("127.0.0.1:12345")
.split(':') .split(':')
.next() .next()
@ -327,21 +383,25 @@ mod tests {
#[test] #[test]
fn test_mentions_regex() { 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 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); let mentions = scrape_text_for_mentions(text);
assert_eq!(mentions[0].name, "tedu".to_string()); assert_eq!(mentions[0].name, "tedu".to_string());
assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string()); assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
assert_eq!(mentions[1].domain, "lemmy_alpha:8540".to_string()); assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
} }
#[test] #[test]
fn test_image() { fn test_image() {
assert!(is_image_content_type("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").is_ok()); actix_rt::System::new("tset_image").block_on(async move {
assert!(is_image_content_type( let client = actix_web::client::Client::default();
"https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20" 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,
.is_err()); "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
)
.await.is_err()
);
});
} }
#[test] #[test]
@ -399,7 +459,7 @@ mod tests {
// These helped with testing // These helped with testing
// #[test] // #[test]
// fn test_iframely() { // fn test_iframely() {
// let res = fetch_iframely("https://www.redspark.nu/?p=15341"); // let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
// assert!(res.is_ok()); // assert!(res.is_ok());
// } // }

View file

@ -4,10 +4,13 @@ extern crate diesel_migrations;
#[macro_use] #[macro_use]
pub extern crate lazy_static; pub extern crate lazy_static;
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
use crate::lemmy_server::actix_web::dev::Service; use crate::lemmy_server::actix_web::dev::Service;
use actix::prelude::*; use actix::prelude::*;
use actix_web::{ use actix_web::{
body::Body, body::Body,
client::Client,
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
http::{ http::{
header::{CACHE_CONTROL, CONTENT_TYPE}, header::{CACHE_CONTROL, CONTENT_TYPE},
@ -20,14 +23,16 @@ use diesel::{
PgConnection, PgConnection,
}; };
use lemmy_server::{ use lemmy_server::{
blocking,
db::code_migrations::run_advanced_migrations, db::code_migrations::run_advanced_migrations,
rate_limit::{rate_limiter::RateLimiter, RateLimit}, rate_limit::{rate_limiter::RateLimiter, RateLimit},
routes::{api, federation, feeds, index, nodeinfo, webfinger}, routes::{api, federation, feeds, index, nodeinfo, webfinger},
settings::Settings, settings::Settings,
websocket::server::*, websocket::server::*,
LemmyError,
}; };
use regex::Regex; use regex::Regex;
use std::{io, sync::Arc}; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
lazy_static! { lazy_static! {
@ -41,7 +46,7 @@ lazy_static! {
embed_migrations!(); embed_migrations!();
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> Result<(), LemmyError> {
env_logger::init(); env_logger::init();
let settings = Settings::get(); let settings = Settings::get();
@ -53,9 +58,12 @@ async fn main() -> io::Result<()> {
.unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url())); .unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url()));
// Run the migrations from code // Run the migrations from code
let conn = pool.get().unwrap(); blocking(&pool, move |conn| {
embedded_migrations::run(&conn).unwrap(); embedded_migrations::run(conn)?;
run_advanced_migrations(&conn).unwrap(); run_advanced_migrations(conn)?;
Ok(()) as Result<(), LemmyError>
})
.await??;
// Set up the rate limiter // Set up the rate limiter
let rate_limiter = RateLimit { let rate_limiter = RateLimit {
@ -63,7 +71,7 @@ async fn main() -> io::Result<()> {
}; };
// Set up websocket server // Set up websocket server
let server = ChatServer::startup(pool.clone(), rate_limiter.clone()).start(); let server = ChatServer::startup(pool.clone(), rate_limiter.clone(), Client::default()).start();
println!( println!(
"Starting http server at {}:{}", "Starting http server at {}:{}",
@ -79,6 +87,7 @@ async fn main() -> io::Result<()> {
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.data(pool.clone()) .data(pool.clone())
.data(server.clone()) .data(server.clone())
.data(Client::default())
// The routes // The routes
.configure(move |cfg| api::config(cfg, &rate_limiter)) .configure(move |cfg| api::config(cfg, &rate_limiter))
.configure(federation::config) .configure(federation::config)
@ -98,7 +107,9 @@ async fn main() -> io::Result<()> {
}) })
.bind((settings.bind, settings.port))? .bind((settings.bind, settings.port))?
.run() .run()
.await .await?;
Ok(())
} }
fn add_cache_headers<S>( fn add_cache_headers<S>(

View file

@ -1,5 +1,5 @@
use super::{IPAddr, Settings}; use super::{IPAddr, Settings};
use crate::{api::APIError, get_ip, settings::RateLimitConfig}; use crate::{get_ip, settings::RateLimitConfig, LemmyError};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use futures::future::{ok, Ready}; use futures::future::{ok, Ready};
use rate_limiter::{RateLimitType, RateLimiter}; use rate_limiter::{RateLimitType, RateLimiter};
@ -15,6 +15,8 @@ pub mod rate_limiter;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RateLimit { pub struct RateLimit {
// it might be reasonable to use a std::sync::Mutex here, since we don't need to lock this
// across await points
pub rate_limiter: Arc<Mutex<RateLimiter>>, pub rate_limiter: Arc<Mutex<RateLimiter>>,
} }
@ -57,17 +59,11 @@ impl RateLimited {
fut: impl Future<Output = Result<T, E>>, fut: impl Future<Output = Result<T, E>>,
) -> Result<T, E> ) -> Result<T, E>
where where
E: From<failure::Error>, E: From<LemmyError>,
{ {
let rate_limit: RateLimitConfig = actix_web::web::block(move || { // Does not need to be blocking because the RwLock in settings never held across await points,
// needs to be in a web::block because the RwLock in settings is from stdlib // and the operation here locks only long enough to clone
Ok(Settings::get().rate_limit) as Result<_, failure::Error> let rate_limit: RateLimitConfig = Settings::get().rate_limit;
})
.await
.map_err(|e| match e {
actix_web::error::BlockingError::Error(e) => e,
_ => APIError::err("Operation canceled").into(),
})?;
// before // before
{ {
@ -83,6 +79,7 @@ impl RateLimited {
false, false,
)?; )?;
drop(limiter);
return fut.await; return fut.await;
} }
RateLimitType::Post => { RateLimitType::Post => {

View file

@ -1,6 +1,5 @@
use super::IPAddr; use super::IPAddr;
use crate::api::APIError; use crate::{api::APIError, LemmyError};
use failure::Error;
use log::debug; use log::debug;
use std::{collections::HashMap, time::SystemTime}; use std::{collections::HashMap, time::SystemTime};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
@ -61,7 +60,7 @@ impl RateLimiter {
rate: i32, rate: i32,
per: i32, per: i32,
check_only: bool, check_only: bool,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
self.insert_ip(ip); self.insert_ip(ip);
if let Some(bucket) = self.buckets.get_mut(&type_) { if let Some(bucket) = self.buckets.get_mut(&type_) {
if let Some(rate_limit) = bucket.get_mut(ip) { if let Some(rate_limit) = bucket.get_mut(ip) {

51
server/src/request.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::LemmyError;
use std::future::Future;
#[derive(Clone, Debug, Fail)]
#[fail(display = "Error sending request, {}", _0)]
struct SendError(pub String);
#[derive(Clone, Debug, Fail)]
#[fail(display = "Error receiving response, {}", _0)]
pub struct RecvError(pub String);
pub async fn retry<F, Fut, T>(f: F) -> Result<T, LemmyError>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<T, actix_web::client::SendRequestError>>,
{
retry_custom(|| async { Ok((f)().await) }).await
}
pub async fn retry_custom<F, Fut, T>(f: F) -> Result<T, LemmyError>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<Result<T, actix_web::client::SendRequestError>, LemmyError>>,
{
let mut response = Err(format_err!("connect timeout").into());
for _ in 0u8..3 {
match (f)().await? {
Ok(t) => return Ok(t),
Err(e) => {
if is_connect_timeout(&e) {
response = Err(SendError(e.to_string()).into());
continue;
}
return Err(SendError(e.to_string()).into());
}
}
}
response
}
fn is_connect_timeout(e: &actix_web::client::SendRequestError) -> bool {
if let actix_web::client::SendRequestError::Connect(e) = e {
if let actix_web::client::ConnectError::Timeout = e {
return true;
}
}
false
}

View file

@ -4,7 +4,7 @@ use crate::{
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
websocket::WebsocketInfo, websocket::WebsocketInfo,
}; };
use actix_web::{error::ErrorBadRequest, *}; use actix_web::{client::Client, error::ErrorBadRequest, *};
use serde::Serialize; use serde::Serialize;
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
@ -150,6 +150,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
async fn perform<Request>( async fn perform<Request>(
data: Request, data: Request,
client: &Client,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
@ -162,9 +163,10 @@ where
id: None, id: None,
}; };
let oper: Oper<Request> = Oper::new(data); let oper: Oper<Request> = Oper::new(data, client.clone());
let res = web::block(move || oper.perform(db.get_ref().to_owned(), Some(ws_info))) let res = oper
.perform(&db, Some(ws_info))
.await .await
.map(|json| HttpResponse::Ok().json(json)) .map(|json| HttpResponse::Ok().json(json))
.map_err(ErrorBadRequest)?; .map_err(ErrorBadRequest)?;
@ -173,6 +175,7 @@ where
async fn route_get<Data>( async fn route_get<Data>(
data: web::Query<Data>, data: web::Query<Data>,
client: web::Data<Client>,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
@ -180,11 +183,12 @@ where
Data: Serialize + Send + 'static, Data: Serialize + Send + 'static,
Oper<Data>: Perform, Oper<Data>: Perform,
{ {
perform::<Data>(data.0, db, chat_server).await perform::<Data>(data.0, &client, db, chat_server).await
} }
async fn route_post<Data>( async fn route_post<Data>(
data: web::Json<Data>, data: web::Json<Data>,
client: web::Data<Client>,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
@ -192,5 +196,5 @@ where
Data: Serialize + Send + 'static, Data: Serialize + Send + 'static,
Oper<Data>: Perform, Oper<Data>: Perform,
{ {
perform::<Data>(data.0, db, chat_server).await perform::<Data>(data.0, &client, db, chat_server).await
} }

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
blocking,
db::{ db::{
comment_view::{ReplyQueryBuilder, ReplyView}, comment_view::{ReplyQueryBuilder, ReplyView},
community::Community, community::Community,
@ -12,6 +13,7 @@ use crate::{
markdown_to_html, markdown_to_html,
routes::DbPoolParam, routes::DbPoolParam,
settings::Settings, settings::Settings,
LemmyError,
}; };
use actix_web::{error::ErrorBadRequest, *}; use actix_web::{error::ErrorBadRequest, *};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
@ -43,21 +45,20 @@ pub fn config(cfg: &mut web::ServiceConfig) {
} }
async fn get_all_feed(info: web::Query<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> { async fn get_all_feed(info: web::Query<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> {
let res = web::block(move || { let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
let conn = db.get()?;
get_feed_all_data(&conn, &get_sort_type(info)?) let rss = blocking(&db, move |conn| get_feed_all_data(conn, &sort_type))
}) .await?
.await .map_err(ErrorBadRequest)?;
.map(|rss| {
Ok(
HttpResponse::Ok() HttpResponse::Ok()
.content_type("application/rss+xml") .content_type("application/rss+xml")
.body(rss) .body(rss),
}) )
.map_err(ErrorBadRequest)?;
Ok(res)
} }
fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, failure::Error> { fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let posts = PostQueryBuilder::create(&conn) let posts = PostQueryBuilder::create(&conn)
@ -85,37 +86,34 @@ async fn get_feed(
info: web::Query<Params>, info: web::Query<Params>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let res = web::block(move || { let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
let conn = db.get()?;
let sort_type = get_sort_type(info)?; let request_type = match path.0.as_ref() {
"u" => RequestType::User,
"c" => RequestType::Community,
"front" => RequestType::Front,
"inbox" => RequestType::Inbox,
_ => return Err(ErrorBadRequest(LemmyError::from(format_err!("wrong_type")))),
};
let request_type = match path.0.as_ref() { let param = path.1.to_owned();
"u" => RequestType::User,
"c" => RequestType::Community,
"front" => RequestType::Front,
"inbox" => RequestType::Inbox,
_ => return Err(format_err!("wrong_type")),
};
let param = path.1.to_owned(); let builder = blocking(&db, move |conn| match request_type {
RequestType::User => get_feed_user(conn, &sort_type, param),
match request_type { RequestType::Community => get_feed_community(conn, &sort_type, param),
RequestType::User => get_feed_user(&conn, &sort_type, param), RequestType::Front => get_feed_front(conn, &sort_type, param),
RequestType::Community => get_feed_community(&conn, &sort_type, param), RequestType::Inbox => get_feed_inbox(conn, param),
RequestType::Front => get_feed_front(&conn, &sort_type, param),
RequestType::Inbox => get_feed_inbox(&conn, param),
}
}) })
.await .await?
.map(|builder| builder.build().unwrap().to_string()) .map_err(ErrorBadRequest)?;
.map(|rss| {
let rss = builder.build().map_err(ErrorBadRequest)?.to_string();
Ok(
HttpResponse::Ok() HttpResponse::Ok()
.content_type("application/rss+xml") .content_type("application/rss+xml")
.body(rss) .body(rss),
}) )
.map_err(ErrorBadRequest)?;
Ok(res)
} }
fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> { fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
@ -130,7 +128,7 @@ fn get_feed_user(
conn: &PgConnection, conn: &PgConnection,
sort_type: &SortType, sort_type: &SortType,
user_name: String, user_name: String,
) -> Result<ChannelBuilder, failure::Error> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user = User_::find_by_username(&conn, &user_name)?; let user = User_::find_by_username(&conn, &user_name)?;
let user_url = user.get_profile_url(); let user_url = user.get_profile_url();
@ -156,7 +154,7 @@ fn get_feed_community(
conn: &PgConnection, conn: &PgConnection,
sort_type: &SortType, sort_type: &SortType,
community_name: String, community_name: String,
) -> Result<ChannelBuilder, failure::Error> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let community = Community::read_from_name(&conn, &community_name)?; let community = Community::read_from_name(&conn, &community_name)?;
@ -185,7 +183,7 @@ fn get_feed_front(
conn: &PgConnection, conn: &PgConnection,
sort_type: &SortType, sort_type: &SortType,
jwt: String, jwt: String,
) -> Result<ChannelBuilder, failure::Error> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user_id = Claims::decode(&jwt)?.claims.id; let user_id = Claims::decode(&jwt)?.claims.id;
@ -210,7 +208,7 @@ fn get_feed_front(
Ok(channel_builder) Ok(channel_builder)
} }
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, failure::Error> { fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user_id = Claims::decode(&jwt)?.claims.id; let user_id = Claims::decode(&jwt)?.claims.id;

View file

@ -1,8 +1,10 @@
use crate::{ use crate::{
apub::get_apub_protocol_string, apub::get_apub_protocol_string,
blocking,
db::site_view::SiteView, db::site_view::SiteView,
routes::DbPoolParam, routes::DbPoolParam,
version, version,
LemmyError,
Settings, Settings,
}; };
use actix_web::{body::Body, error::ErrorBadRequest, *}; use actix_web::{body::Body, error::ErrorBadRequest, *};
@ -15,7 +17,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/.well-known/nodeinfo", web::get().to(node_info_well_known)); .route("/.well-known/nodeinfo", web::get().to(node_info_well_known));
} }
async fn node_info_well_known() -> Result<HttpResponse<Body>, failure::Error> { async fn node_info_well_known() -> Result<HttpResponse<Body>, LemmyError> {
let node_info = NodeInfoWellKnown { let node_info = NodeInfoWellKnown {
links: NodeInfoWellKnownLinks { links: NodeInfoWellKnownLinks {
rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?, rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?,
@ -30,38 +32,34 @@ async fn node_info_well_known() -> Result<HttpResponse<Body>, failure::Error> {
} }
async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> { async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
let res = web::block(move || { let site_view = blocking(&db, SiteView::read)
let conn = db.get()?; .await?
let site_view = match SiteView::read(&conn) { .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?;
Ok(site_view) => site_view,
Err(_) => return Err(format_err!("not_found")), let protocols = if Settings::get().federation.enabled {
}; vec!["activitypub".to_string()]
let protocols = if Settings::get().federation.enabled { } else {
vec!["activitypub".to_string()] vec![]
} else { };
vec![]
}; let json = NodeInfo {
Ok(NodeInfo { version: "2.0".to_string(),
version: "2.0".to_string(), software: NodeInfoSoftware {
software: NodeInfoSoftware { name: "lemmy".to_string(),
name: "lemmy".to_string(), version: version::VERSION.to_string(),
version: version::VERSION.to_string(), },
protocols,
usage: NodeInfoUsage {
users: NodeInfoUsers {
total: site_view.number_of_users,
}, },
protocols, local_posts: site_view.number_of_posts,
usage: NodeInfoUsage { local_comments: site_view.number_of_comments,
users: NodeInfoUsers { open_registrations: site_view.open_registration,
total: site_view.number_of_users, },
}, };
local_posts: site_view.number_of_posts,
local_comments: site_view.number_of_comments, Ok(HttpResponse::Ok().json(json))
open_registrations: site_view.open_registration,
},
})
})
.await
.map(|json| HttpResponse::Ok().json(json))
.map_err(ErrorBadRequest)?;
Ok(res)
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -1,6 +1,8 @@
use crate::{ use crate::{
blocking,
db::{community::Community, user::User_}, db::{community::Community, user::User_},
routes::DbPoolParam, routes::DbPoolParam,
LemmyError,
Settings, Settings,
}; };
use actix_web::{error::ErrorBadRequest, web::Query, *}; use actix_web::{error::ErrorBadRequest, web::Query, *};
@ -61,64 +63,58 @@ async fn get_webfinger_response(
info: Query<Params>, info: Query<Params>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let res = web::block(move || { let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX
let conn = db.get()?; .captures(&info.resource)
.map(|c| c.get(1))
.flatten();
let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX let user_regex_parsed = WEBFINGER_USER_REGEX
.captures(&info.resource) .captures(&info.resource)
.map(|c| c.get(1)) .map(|c| c.get(1))
.flatten(); .flatten();
let user_regex_parsed = WEBFINGER_USER_REGEX let url = if let Some(community_name) = community_regex_parsed {
.captures(&info.resource) let community_name = community_name.as_str().to_owned();
.map(|c| c.get(1)) // Make sure the requested community exists.
.flatten(); blocking(&db, move |conn| {
Community::read_from_name(conn, &community_name)
})
.await?
.map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?
.actor_id
} else if let Some(user_name) = user_regex_parsed {
let user_name = user_name.as_str().to_owned();
// Make sure the requested user exists.
blocking(&db, move |conn| User_::read_from_name(conn, &user_name))
.await?
.map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?
.actor_id
} else {
return Err(ErrorBadRequest(LemmyError::from(format_err!("not_found"))));
};
let url = if let Some(community_name) = community_regex_parsed { let json = WebFingerResponse {
// Make sure the requested community exists. subject: info.resource.to_owned(),
let community = match Community::read_from_name(&conn, &community_name.as_str()) { aliases: vec![url.to_owned()],
Ok(o) => o, links: vec![
Err(_) => return Err(format_err!("not_found")), WebFingerLink {
}; rel: Some("http://webfinger.net/rel/profile-page".to_string()),
community.actor_id type_: Some("text/html".to_string()),
} else if let Some(user_name) = user_regex_parsed { href: Some(url.to_owned()),
// Make sure the requested user exists. template: None,
let user = match User_::read_from_name(&conn, &user_name.as_str()) { },
Ok(o) => o, WebFingerLink {
Err(_) => return Err(format_err!("not_found")), rel: Some("self".to_string()),
}; type_: Some("application/activity+json".to_string()),
user.actor_id href: Some(url),
} else { template: None,
return Err(format_err!("not_found")); }, // TODO: this also needs to return the subscribe link once that's implemented
}; //{
// "rel": "http://ostatus.org/schema/1.0/subscribe",
// "template": "https://my_instance.com/authorize_interaction?uri={uri}"
//}
],
};
let wf_res = WebFingerResponse { Ok(HttpResponse::Ok().json(json))
subject: info.resource.to_owned(),
aliases: vec![url.to_owned()],
links: vec![
WebFingerLink {
rel: Some("http://webfinger.net/rel/profile-page".to_string()),
type_: Some("text/html".to_string()),
href: Some(url.to_owned()),
template: None,
},
WebFingerLink {
rel: Some("self".to_string()),
type_: Some("application/activity+json".to_string()),
href: Some(url),
template: None,
}, // TODO: this also needs to return the subscribe link once that's implemented
//{
// "rel": "http://ostatus.org/schema/1.0/subscribe",
// "template": "https://my_instance.com/authorize_interaction?uri={uri}"
//}
],
};
Ok(wf_res)
})
.await
.map(|json| HttpResponse::Ok().json(json))
.map_err(ErrorBadRequest)?;
Ok(res)
} }

View file

@ -1,5 +1,5 @@
use crate::LemmyError;
use config::{Config, ConfigError, Environment, File}; use config::{Config, ConfigError, Environment, File};
use failure::Error;
use serde::Deserialize; use serde::Deserialize;
use std::{env, fs, net::IpAddr, sync::RwLock}; use std::{env, fs, net::IpAddr, sync::RwLock};
@ -118,11 +118,11 @@ impl Settings {
format!("{}/api/v1", self.hostname) format!("{}/api/v1", self.hostname)
} }
pub fn read_config_file() -> Result<String, Error> { pub fn read_config_file() -> Result<String, LemmyError> {
Ok(fs::read_to_string(CONFIG_FILE)?) Ok(fs::read_to_string(CONFIG_FILE)?)
} }
pub fn save_config_file(data: &str) -> Result<String, Error> { pub fn save_config_file(data: &str) -> Result<String, LemmyError> {
fs::write(CONFIG_FILE, data)?; fs::write(CONFIG_FILE, data)?;
// Reload the new settings // Reload the new settings

View file

@ -6,7 +6,6 @@ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,
}; };
use failure::Error;
use log::{error, info}; use log::{error, info};
use rand::{rngs::ThreadRng, Rng}; use rand::{rngs::ThreadRng, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -9,10 +9,13 @@ use crate::{
websocket::UserOperation, websocket::UserOperation,
CommunityId, CommunityId,
ConnectionId, ConnectionId,
DbPool,
IPAddr, IPAddr,
LemmyError,
PostId, PostId,
UserId, UserId,
}; };
use actix_web::client::Client;
/// Chat server sends this messages to session /// Chat server sends this messages to session
#[derive(Message)] #[derive(Message)]
@ -154,12 +157,16 @@ pub struct ChatServer {
/// Rate limiting based on rate type and IP addr /// Rate limiting based on rate type and IP addr
rate_limiter: RateLimit, rate_limiter: RateLimit,
/// An HTTP Client
client: Client,
} }
impl ChatServer { impl ChatServer {
pub fn startup( pub fn startup(
pool: Pool<ConnectionManager<PgConnection>>, pool: Pool<ConnectionManager<PgConnection>>,
rate_limiter: RateLimit, rate_limiter: RateLimit,
client: Client,
) -> ChatServer { ) -> ChatServer {
ChatServer { ChatServer {
sessions: HashMap::new(), sessions: HashMap::new(),
@ -169,6 +176,7 @@ impl ChatServer {
rng: rand::thread_rng(), rng: rand::thread_rng(),
pool, pool,
rate_limiter, rate_limiter,
client,
} }
} }
@ -236,7 +244,7 @@ impl ChatServer {
response: &Response, response: &Response,
post_id: PostId, post_id: PostId,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -260,7 +268,7 @@ impl ChatServer {
response: &Response, response: &Response,
community_id: CommunityId, community_id: CommunityId,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -283,7 +291,7 @@ impl ChatServer {
op: &UserOperation, op: &UserOperation,
response: &Response, response: &Response,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -305,7 +313,7 @@ impl ChatServer {
response: &Response, response: &Response,
recipient_id: UserId, recipient_id: UserId,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -328,7 +336,7 @@ impl ChatServer {
user_operation: &UserOperation, user_operation: &UserOperation,
comment: &CommentResponse, comment: &CommentResponse,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
let mut comment_reply_sent = comment.clone(); let mut comment_reply_sent = comment.clone();
comment_reply_sent.comment.my_vote = None; comment_reply_sent.comment.my_vote = None;
comment_reply_sent.comment.user_id = None; comment_reply_sent.comment.user_id = None;
@ -366,7 +374,7 @@ impl ChatServer {
user_operation: &UserOperation, user_operation: &UserOperation,
post: &PostResponse, post: &PostResponse,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
let community_id = post.post.community_id; let community_id = post.post.community_id;
// Don't send my data with it // Don't send my data with it
@ -394,7 +402,7 @@ impl ChatServer {
&mut self, &mut self,
msg: StandardMessage, msg: StandardMessage,
ctx: &mut Context<Self>, ctx: &mut Context<Self>,
) -> impl Future<Output = Result<String, Error>> { ) -> impl Future<Output = Result<String, LemmyError>> {
let addr = ctx.address(); let addr = ctx.address();
let pool = self.pool.clone(); let pool = self.pool.clone();
let rate_limiter = self.rate_limiter.clone(); let rate_limiter = self.rate_limiter.clone();
@ -404,6 +412,7 @@ impl ChatServer {
None => "blank_ip".to_string(), None => "blank_ip".to_string(),
}; };
let client = self.client.clone();
async move { async move {
let msg = msg; let msg = msg;
let json: Value = serde_json::from_str(&msg.msg)?; let json: Value = serde_json::from_str(&msg.msg)?;
@ -414,482 +423,109 @@ impl ChatServer {
let user_operation: UserOperation = UserOperation::from_str(&op)?; let user_operation: UserOperation = UserOperation::from_str(&op)?;
let args = Args {
client,
pool,
rate_limiter,
chatserver: addr,
id: msg.id,
ip,
op: user_operation.clone(),
data,
};
match user_operation { match user_operation {
// User ops // User ops
UserOperation::Login => { UserOperation::Login => do_user_operation::<Login>(args).await,
do_user_operation::<Login>(pool, rate_limiter, addr, msg.id, ip, user_operation, data) UserOperation::Register => do_user_operation::<Register>(args).await,
.await UserOperation::GetUserDetails => do_user_operation::<GetUserDetails>(args).await,
} UserOperation::GetReplies => do_user_operation::<GetReplies>(args).await,
UserOperation::Register => { UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
do_user_operation::<Register>(pool, rate_limiter, addr, msg.id, ip, user_operation, data) UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
.await UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
} UserOperation::EditUserMention => do_user_operation::<EditUserMention>(args).await,
UserOperation::GetUserDetails => { UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
do_user_operation::<GetUserDetails>( UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
pool, UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
rate_limiter, UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetReplies => {
do_user_operation::<GetReplies>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::AddAdmin => {
do_user_operation::<AddAdmin>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::BanUser => {
do_user_operation::<BanUser>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetUserMentions => {
do_user_operation::<GetUserMentions>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditUserMention => {
do_user_operation::<EditUserMention>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::MarkAllAsRead => {
do_user_operation::<MarkAllAsRead>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::DeleteAccount => {
do_user_operation::<DeleteAccount>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::PasswordReset => {
do_user_operation::<PasswordReset>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::PasswordChange => {
do_user_operation::<PasswordChange>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::CreatePrivateMessage => { UserOperation::CreatePrivateMessage => {
do_user_operation::<CreatePrivateMessage>( do_user_operation::<CreatePrivateMessage>(args).await
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditPrivateMessage => {
do_user_operation::<EditPrivateMessage>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetPrivateMessages => {
do_user_operation::<GetPrivateMessages>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::UserJoin => {
do_user_operation::<UserJoin>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::SaveUserSettings => {
do_user_operation::<SaveUserSettings>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
} }
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Site ops // Site ops
UserOperation::GetModlog => { UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
do_user_operation::<GetModlog>(pool, rate_limiter, addr, msg.id, ip, user_operation, data) UserOperation::CreateSite => do_user_operation::<CreateSite>(args).await,
.await UserOperation::EditSite => do_user_operation::<EditSite>(args).await,
} UserOperation::GetSite => do_user_operation::<GetSite>(args).await,
UserOperation::CreateSite => { UserOperation::GetSiteConfig => do_user_operation::<GetSiteConfig>(args).await,
do_user_operation::<CreateSite>( UserOperation::SaveSiteConfig => do_user_operation::<SaveSiteConfig>(args).await,
pool, UserOperation::Search => do_user_operation::<Search>(args).await,
rate_limiter, UserOperation::TransferCommunity => do_user_operation::<TransferCommunity>(args).await,
addr, UserOperation::TransferSite => do_user_operation::<TransferSite>(args).await,
msg.id, UserOperation::ListCategories => do_user_operation::<ListCategories>(args).await,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditSite => {
do_user_operation::<EditSite>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetSite => {
do_user_operation::<GetSite>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetSiteConfig => {
do_user_operation::<GetSiteConfig>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::SaveSiteConfig => {
do_user_operation::<SaveSiteConfig>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::Search => {
do_user_operation::<Search>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::TransferCommunity => {
do_user_operation::<TransferCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::TransferSite => {
do_user_operation::<TransferSite>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::ListCategories => {
do_user_operation::<ListCategories>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
// Community ops // Community ops
UserOperation::GetCommunity => { UserOperation::GetCommunity => do_user_operation::<GetCommunity>(args).await,
do_user_operation::<GetCommunity>( UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
pool, UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
rate_limiter, UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
addr, UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::ListCommunities => {
do_user_operation::<ListCommunities>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::CreateCommunity => {
do_user_operation::<CreateCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditCommunity => {
do_user_operation::<EditCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::FollowCommunity => {
do_user_operation::<FollowCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetFollowedCommunities => { UserOperation::GetFollowedCommunities => {
do_user_operation::<GetFollowedCommunities>( do_user_operation::<GetFollowedCommunities>(args).await
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::BanFromCommunity => {
do_user_operation::<BanFromCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::AddModToCommunity => {
do_user_operation::<AddModToCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
} }
UserOperation::BanFromCommunity => do_user_operation::<BanFromCommunity>(args).await,
UserOperation::AddModToCommunity => do_user_operation::<AddModToCommunity>(args).await,
// Post ops // Post ops
UserOperation::CreatePost => { UserOperation::CreatePost => do_user_operation::<CreatePost>(args).await,
do_user_operation::<CreatePost>( UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
pool, UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
rate_limiter, UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
addr, UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
msg.id, UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetPost => {
do_user_operation::<GetPost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetPosts => {
do_user_operation::<GetPosts>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::EditPost => {
do_user_operation::<EditPost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::CreatePostLike => {
do_user_operation::<CreatePostLike>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::SavePost => {
do_user_operation::<SavePost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
// Comment ops // Comment ops
UserOperation::CreateComment => { UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
do_user_operation::<CreateComment>( UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
pool, UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
rate_limiter, UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
addr, UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditComment => {
do_user_operation::<EditComment>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::SaveComment => {
do_user_operation::<SaveComment>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetComments => {
do_user_operation::<GetComments>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::CreateCommentLike => {
do_user_operation::<CreateCommentLike>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
} }
} }
} }
} }
async fn do_user_operation<'a, Data>( struct Args<'a> {
pool: Pool<ConnectionManager<PgConnection>>, client: Client,
pool: DbPool,
rate_limiter: RateLimit, rate_limiter: RateLimit,
chatserver: Addr<ChatServer>, chatserver: Addr<ChatServer>,
id: ConnectionId, id: ConnectionId,
ip: IPAddr, ip: IPAddr,
op: UserOperation, op: UserOperation,
data: &str, data: &'a str,
) -> Result<String, Error> }
async fn do_user_operation<'a, 'b, Data>(args: Args<'b>) -> Result<String, LemmyError>
where where
for<'de> Data: Deserialize<'de> + 'a, for<'de> Data: Deserialize<'de> + 'a,
Oper<Data>: Perform, Oper<Data>: Perform,
{ {
let Args {
client,
pool,
rate_limiter,
chatserver,
id,
ip,
op,
data,
} = args;
let ws_info = WebsocketInfo { let ws_info = WebsocketInfo {
chatserver, chatserver,
id: Some(id), id: Some(id),
@ -898,17 +534,14 @@ where
let data = data.to_string(); let data = data.to_string();
let op2 = op.clone(); let op2 = op.clone();
let client = client.clone();
let fut = async move { let fut = async move {
actix_web::web::block(move || { let pool = pool.clone();
let parsed_data: Data = serde_json::from_str(&data)?; let parsed_data: Data = serde_json::from_str(&data)?;
let res = Oper::new(parsed_data).perform(pool, Some(ws_info))?; let res = Oper::new(parsed_data, client)
to_json_string(&op, &res) .perform(&pool, Some(ws_info))
}) .await?;
.await to_json_string(&op, &res)
.map_err(|e| match e {
actix_web::error::BlockingError::Error(e) => e,
_ => APIError::err("Operation canceled").into(),
})
}; };
match op2 { match op2 {
@ -1109,7 +742,7 @@ struct WebsocketResponse<T> {
data: T, data: T,
} }
fn to_json_string<Response>(op: &UserOperation, data: &Response) -> Result<String, Error> fn to_json_string<Response>(op: &UserOperation, data: &Response) -> Result<String, LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {

View file

@ -124,10 +124,10 @@ describe('main', () => {
}); });
describe('follow_accept', () => { describe('follow_accept', () => {
test('/u/lemmy_alpha follows and accepts lemmy_beta/c/main', async () => { test('/u/lemmy_alpha follows and accepts lemmy-beta/c/main', async () => {
// Make sure lemmy_beta/c/main is cached on lemmy_alpha // Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url // Use short-hand search url
let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy_beta:8550&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',
@ -215,7 +215,7 @@ describe('main', () => {
// Also make G follow B // Also make G follow B
// Use short-hand search url // Use short-hand search url
let searchUrlG = `${lemmyGammaApiUrl}/search?q=!main@lemmy_beta:8550&type_=All&sort=TopAll`; let searchUrlG = `${lemmyGammaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
let searchResponseG: SearchResponse = await fetch(searchUrlG, { let searchResponseG: SearchResponse = await fetch(searchUrlG, {
method: 'GET', method: 'GET',
@ -449,7 +449,7 @@ describe('main', () => {
// Lemmy alpha responds to their own comment, but mentions lemmy beta. // Lemmy alpha responds to their own comment, but mentions lemmy beta.
// Make sure lemmy beta gets that in their inbox. // Make sure lemmy beta gets that in their inbox.
let mentionContent = 'A test mention of @lemmy_beta@lemmy_beta:8550'; let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8550';
let mentionCommentForm: CommentForm = { let mentionCommentForm: CommentForm = {
content: mentionContent, content: mentionContent,
post_id: 2, post_id: 2,
@ -550,7 +550,7 @@ describe('main', () => {
expect(createCommunityRes.community.name).toBe(communityName); expect(createCommunityRes.community.name).toBe(communityName);
// Cache it on lemmy_alpha // Cache it on lemmy_alpha
let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/${communityName}&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy-beta:8550/c/${communityName}&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',
}).then(d => d.json()); }).then(d => d.json());
@ -826,7 +826,7 @@ describe('main', () => {
expect(createCommunityRes.community.name).toBe(communityName); expect(createCommunityRes.community.name).toBe(communityName);
// Cache it on lemmy_alpha // Cache it on lemmy_alpha
let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/${communityName}&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy-beta:8550/c/${communityName}&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',
}).then(d => d.json()); }).then(d => d.json());
@ -1278,7 +1278,7 @@ describe('main', () => {
// Create a test comment on Gamma, make sure it gets announced to alpha // Create a test comment on Gamma, make sure it gets announced to alpha
let commentContent = let commentContent =
'A jest test federated comment announce, lets mention @lemmy_beta@lemmy_beta:8550'; 'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8550';
let commentForm: CommentForm = { let commentForm: CommentForm = {
content: commentContent, content: commentContent,
@ -1417,7 +1417,7 @@ describe('main', () => {
expect(createChildCommentRes.comment.content).toBe(childCommentContent); expect(createChildCommentRes.comment.content).toBe(childCommentContent);
// Follow again, for other tests // Follow again, for other tests
let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy_beta:8550&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',