Move api code into workspace
This commit is contained in:
parent
8b6860881c
commit
d40eb809dd
17 changed files with 358 additions and 263 deletions
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -1787,6 +1787,50 @@ version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lemmy_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"actix",
|
||||||
|
"actix-rt",
|
||||||
|
"actix-web",
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"awc",
|
||||||
|
"background-jobs",
|
||||||
|
"base64 0.12.3",
|
||||||
|
"bcrypt",
|
||||||
|
"captcha",
|
||||||
|
"chrono",
|
||||||
|
"diesel",
|
||||||
|
"futures",
|
||||||
|
"http",
|
||||||
|
"http-signature-normalization-actix",
|
||||||
|
"itertools",
|
||||||
|
"jsonwebtoken",
|
||||||
|
"lazy_static",
|
||||||
|
"lemmy_apub",
|
||||||
|
"lemmy_db",
|
||||||
|
"lemmy_rate_limit",
|
||||||
|
"lemmy_structs",
|
||||||
|
"lemmy_utils",
|
||||||
|
"lemmy_websocket",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"percent-encoding",
|
||||||
|
"rand 0.7.3",
|
||||||
|
"reqwest",
|
||||||
|
"serde 1.0.116",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"uuid 0.8.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lemmy_apub"
|
name = "lemmy_apub"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1887,6 +1931,7 @@ dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"lemmy_api",
|
||||||
"lemmy_apub",
|
"lemmy_apub",
|
||||||
"lemmy_db",
|
"lemmy_db",
|
||||||
"lemmy_rate_limit",
|
"lemmy_rate_limit",
|
||||||
|
|
|
@ -8,6 +8,7 @@ lto = true
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
"lemmy_api",
|
||||||
"lemmy_apub",
|
"lemmy_apub",
|
||||||
"lemmy_utils",
|
"lemmy_utils",
|
||||||
"lemmy_db",
|
"lemmy_db",
|
||||||
|
@ -17,12 +18,13 @@ members = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
lemmy_api = { path = "./lemmy_api" }
|
||||||
|
lemmy_apub = { path = "./lemmy_apub" }
|
||||||
lemmy_utils = { path = "./lemmy_utils" }
|
lemmy_utils = { path = "./lemmy_utils" }
|
||||||
lemmy_db = { path = "./lemmy_db" }
|
lemmy_db = { path = "./lemmy_db" }
|
||||||
lemmy_structs = { path = "./lemmy_structs" }
|
lemmy_structs = { path = "./lemmy_structs" }
|
||||||
lemmy_rate_limit = { path = "./lemmy_rate_limit" }
|
lemmy_rate_limit = { path = "./lemmy_rate_limit" }
|
||||||
lemmy_websocket = { path = "./lemmy_websocket" }
|
lemmy_websocket = { path = "./lemmy_websocket" }
|
||||||
lemmy_apub = { path = "./lemmy_apub" }
|
|
||||||
diesel = "1.4"
|
diesel = "1.4"
|
||||||
diesel_migrations = "1.4"
|
diesel_migrations = "1.4"
|
||||||
bcrypt = "0.8"
|
bcrypt = "0.8"
|
||||||
|
|
|
@ -13,8 +13,8 @@ third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||||
# Setting the version on the front end
|
# Setting the version on the front end
|
||||||
cd ../../
|
cd ../../
|
||||||
# Setting the version on the backend
|
# Setting the version on the backend
|
||||||
echo "pub const VERSION: &str = \"$new_tag\";" > "src/version.rs"
|
echo "pub const VERSION: &str = \"$new_tag\";" > "lemmy_api/src/version.rs"
|
||||||
git add "src/version.rs"
|
git add "lemmy_api/src/version.rs"
|
||||||
# Setting the version for Ansible
|
# Setting the version for Ansible
|
||||||
echo $new_tag > "ansible/VERSION"
|
echo $new_tag > "ansible/VERSION"
|
||||||
git add "ansible/VERSION"
|
git add "ansible/VERSION"
|
||||||
|
|
45
lemmy_api/Cargo.toml
Normal file
45
lemmy_api/Cargo.toml
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
[package]
|
||||||
|
name = "lemmy_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Felix Ableitner <me@nutomic.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lemmy_apub = { path = "../lemmy_apub" }
|
||||||
|
lemmy_utils = { path = "../lemmy_utils" }
|
||||||
|
lemmy_db = { path = "../lemmy_db" }
|
||||||
|
lemmy_structs = { path = "../lemmy_structs" }
|
||||||
|
lemmy_rate_limit = { path = "../lemmy_rate_limit" }
|
||||||
|
lemmy_websocket = { path = "../lemmy_websocket" }
|
||||||
|
diesel = "1.4"
|
||||||
|
bcrypt = "0.8"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
serde_json = { version = "1.0", features = ["preserve_order"]}
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
actix = "0.10"
|
||||||
|
actix-web = { version = "3.0", default-features = false }
|
||||||
|
actix-rt = { version = "1.1", default-features = false }
|
||||||
|
awc = { version = "2.0", default-features = false }
|
||||||
|
log = "0.4"
|
||||||
|
rand = "0.7"
|
||||||
|
strum = "0.19"
|
||||||
|
strum_macros = "0.19"
|
||||||
|
jsonwebtoken = "7.0"
|
||||||
|
lazy_static = "1.3"
|
||||||
|
url = { version = "2.1", features = ["serde"] }
|
||||||
|
percent-encoding = "2.1"
|
||||||
|
openssl = "0.10"
|
||||||
|
http = "0.2"
|
||||||
|
http-signature-normalization-actix = { version = "0.4", default-features = false, features = ["sha-2"] }
|
||||||
|
base64 = "0.12"
|
||||||
|
tokio = "0.2"
|
||||||
|
futures = "0.3"
|
||||||
|
itertools = "0.9"
|
||||||
|
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||||
|
sha2 = "0.9"
|
||||||
|
async-trait = "0.1"
|
||||||
|
captcha = "0.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
background-jobs = " 0.8"
|
||||||
|
reqwest = { version = "0.10", features = ["json"] }
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::api::{
|
use crate::{
|
||||||
check_community_ban,
|
check_community_ban,
|
||||||
get_post,
|
get_post,
|
||||||
get_user_from_jwt,
|
get_user_from_jwt,
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform};
|
use crate::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use lemmy_apub::ActorType;
|
use lemmy_apub::ActorType;
|
|
@ -1,16 +1,29 @@
|
||||||
use crate::{api::claims::Claims, DbPool};
|
use crate::claims::Claims;
|
||||||
use actix_web::{web, web::Data};
|
use actix_web::{web, web::Data};
|
||||||
|
use anyhow::anyhow;
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
community::Community,
|
community::Community,
|
||||||
community_view::CommunityUserBanView,
|
community_view::CommunityUserBanView,
|
||||||
post::Post,
|
post::Post,
|
||||||
user::User_,
|
user::User_,
|
||||||
Crud,
|
Crud,
|
||||||
|
DbPool,
|
||||||
};
|
};
|
||||||
use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*};
|
use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*};
|
||||||
use lemmy_utils::{APIError, ConnectionId, LemmyError};
|
use lemmy_utils::{
|
||||||
|
apub::get_apub_protocol_string,
|
||||||
|
request::{retry, RecvError},
|
||||||
|
settings::Settings,
|
||||||
|
APIError,
|
||||||
|
ConnectionId,
|
||||||
|
LemmyError,
|
||||||
|
};
|
||||||
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
|
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
|
||||||
|
use log::error;
|
||||||
|
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||||
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub mod claims;
|
pub mod claims;
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
|
@ -18,6 +31,7 @@ pub mod community;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
pub trait Perform {
|
pub trait Perform {
|
||||||
|
@ -30,7 +44,7 @@ pub trait Perform {
|
||||||
) -> Result<Self::Response, LemmyError>;
|
) -> Result<Self::Response, LemmyError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::api) async fn is_mod_or_admin(
|
pub(in crate) async fn is_mod_or_admin(
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
community_id: i32,
|
community_id: i32,
|
||||||
|
@ -52,17 +66,14 @@ pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::api) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
|
pub(in crate) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
|
||||||
match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
|
match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
|
||||||
Ok(post) => Ok(post),
|
Ok(post) => Ok(post),
|
||||||
Err(_e) => Err(APIError::err("couldnt_find_post").into()),
|
Err(_e) => Err(APIError::err("couldnt_find_post").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::api) async fn get_user_from_jwt(
|
pub(in crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_, LemmyError> {
|
||||||
jwt: &str,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<User_, LemmyError> {
|
|
||||||
let claims = match Claims::decode(&jwt) {
|
let claims = match Claims::decode(&jwt) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
@ -76,7 +87,7 @@ pub(in crate::api) async fn get_user_from_jwt(
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::api) async fn get_user_from_jwt_opt(
|
pub(in crate) async fn get_user_from_jwt_opt(
|
||||||
jwt: &Option<String>,
|
jwt: &Option<String>,
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
) -> Result<Option<User_>, LemmyError> {
|
) -> Result<Option<User_>, LemmyError> {
|
||||||
|
@ -86,7 +97,7 @@ pub(in crate::api) async fn get_user_from_jwt_opt(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::api) async fn check_community_ban(
|
pub(in crate) async fn check_community_ban(
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
community_id: i32,
|
community_id: i32,
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
|
@ -272,3 +283,227 @@ where
|
||||||
.await?;
|
.await?;
|
||||||
serialize_websocket_message(&op, &res)
|
serialize_websocket_message(&op, &res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
|
||||||
|
let mut built_text = String::new();
|
||||||
|
|
||||||
|
// Building proper speech text for espeak
|
||||||
|
for mut c in captcha.chars() {
|
||||||
|
let new_str = if c.is_alphabetic() {
|
||||||
|
if c.is_lowercase() {
|
||||||
|
c.make_ascii_uppercase();
|
||||||
|
format!("lower case {} ... ", c)
|
||||||
|
} else {
|
||||||
|
c.make_ascii_uppercase();
|
||||||
|
format!("capital {} ... ", c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("{} ...", c)
|
||||||
|
};
|
||||||
|
|
||||||
|
built_text.push_str(&new_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
espeak_wav_base64(&built_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
|
||||||
|
// Make a temp file path
|
||||||
|
let uuid = uuid::Uuid::new_v4().to_string();
|
||||||
|
let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
|
||||||
|
|
||||||
|
// Write the wav file
|
||||||
|
Command::new("espeak")
|
||||||
|
.arg("-w")
|
||||||
|
.arg(&file_path)
|
||||||
|
.arg(text)
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
// Read the wav file bytes
|
||||||
|
let bytes = std::fs::read(&file_path)?;
|
||||||
|
|
||||||
|
// Delete the file
|
||||||
|
std::fs::remove_file(file_path)?;
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
let base64 = base64::encode(bytes);
|
||||||
|
|
||||||
|
Ok(base64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub(crate) struct IframelyResponse {
|
||||||
|
title: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
thumbnail_url: Option<String>,
|
||||||
|
html: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn fetch_iframely(
|
||||||
|
client: &Client,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<IframelyResponse, LemmyError> {
|
||||||
|
let fetch_url = format!("http://iframely/oembed?url={}", url);
|
||||||
|
|
||||||
|
let response = retry(|| client.get(&fetch_url).send()).await?;
|
||||||
|
|
||||||
|
let res: IframelyResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| RecvError(e.to_string()))?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub(crate) struct PictrsResponse {
|
||||||
|
files: Vec<PictrsFile>,
|
||||||
|
msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub(crate) struct PictrsFile {
|
||||||
|
file: String,
|
||||||
|
delete_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn fetch_pictrs(
|
||||||
|
client: &Client,
|
||||||
|
image_url: &str,
|
||||||
|
) -> Result<PictrsResponse, LemmyError> {
|
||||||
|
is_image_content_type(client, image_url).await?;
|
||||||
|
|
||||||
|
let fetch_url = format!(
|
||||||
|
"http://pictrs:8080/image/download?url={}",
|
||||||
|
utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = retry(|| client.get(&fetch_url).send()).await?;
|
||||||
|
|
||||||
|
let response: PictrsResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| RecvError(e.to_string()))?;
|
||||||
|
|
||||||
|
if response.msg == "ok" {
|
||||||
|
Ok(response)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("{}", &response.msg).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_iframely_and_pictrs_data(
|
||||||
|
client: &Client,
|
||||||
|
url: Option<String>,
|
||||||
|
) -> (
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
) {
|
||||||
|
match &url {
|
||||||
|
Some(url) => {
|
||||||
|
// Fetch iframely data
|
||||||
|
let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
|
||||||
|
match fetch_iframely(client, url).await {
|
||||||
|
Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
|
||||||
|
Err(e) => {
|
||||||
|
error!("iframely err: {}", e);
|
||||||
|
(None, None, None, None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch pictrs thumbnail
|
||||||
|
let pictrs_hash = match iframely_thumbnail_url {
|
||||||
|
Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
|
||||||
|
Ok(res) => Some(res.files[0].file.to_owned()),
|
||||||
|
Err(e) => {
|
||||||
|
error!("pictrs err: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Try to generate a small thumbnail if iframely is not supported
|
||||||
|
None => match fetch_pictrs(client, &url).await {
|
||||||
|
Ok(res) => Some(res.files[0].file.to_owned()),
|
||||||
|
Err(e) => {
|
||||||
|
error!("pictrs err: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// The full urls are necessary for federation
|
||||||
|
let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
|
||||||
|
Some(format!(
|
||||||
|
"{}://{}/pictrs/image/{}",
|
||||||
|
get_apub_protocol_string(),
|
||||||
|
Settings::get().hostname,
|
||||||
|
pictrs_hash
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
iframely_title,
|
||||||
|
iframely_description,
|
||||||
|
iframely_html,
|
||||||
|
pictrs_thumbnail,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => (None, None, None, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
|
||||||
|
let response = retry(|| client.get(test).send()).await?;
|
||||||
|
|
||||||
|
if response
|
||||||
|
.headers()
|
||||||
|
.get("Content-Type")
|
||||||
|
.ok_or_else(|| anyhow!("No Content-Type header"))?
|
||||||
|
.to_str()?
|
||||||
|
.starts_with("image/")
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not an image type.").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{captcha_espeak_wav_base64, is_image_content_type};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_image() {
|
||||||
|
actix_rt::System::new("tset_image").block_on(async move {
|
||||||
|
let client = reqwest::Client::default();
|
||||||
|
assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
|
||||||
|
assert!(is_image_content_type(&client,
|
||||||
|
"https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
|
||||||
|
)
|
||||||
|
.await.is_err()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_espeak() {
|
||||||
|
assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
// These helped with testing
|
||||||
|
// #[test]
|
||||||
|
// fn test_iframely() {
|
||||||
|
// let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
|
||||||
|
// assert!(res.is_ok());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn test_pictshare() {
|
||||||
|
// let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
|
||||||
|
// assert!(res.is_ok());
|
||||||
|
// let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
|
||||||
|
// assert!(res_other.is_err());
|
||||||
|
// }
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{check_community_ban, get_user_from_jwt, get_user_from_jwt_opt, is_mod_or_admin, Perform},
|
check_community_ban,
|
||||||
fetch_iframely_and_pictrs_data,
|
fetch_iframely_and_pictrs_data,
|
||||||
|
get_user_from_jwt,
|
||||||
|
get_user_from_jwt_opt,
|
||||||
|
is_mod_or_admin,
|
||||||
|
Perform,
|
||||||
};
|
};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use lemmy_apub::{ApubLikeableType, ApubObjectType};
|
use lemmy_apub::{ApubLikeableType, ApubObjectType};
|
|
@ -1,7 +1,4 @@
|
||||||
use crate::{
|
use crate::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, version, Perform};
|
||||||
api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, Perform},
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use lemmy_apub::fetcher::search_by_apub_id;
|
use lemmy_apub::fetcher::search_by_apub_id;
|
|
@ -1,6 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{claims::Claims, get_user_from_jwt, get_user_from_jwt_opt, is_admin, Perform},
|
|
||||||
captcha_espeak_wav_base64,
|
captcha_espeak_wav_base64,
|
||||||
|
claims::Claims,
|
||||||
|
get_user_from_jwt,
|
||||||
|
get_user_from_jwt_opt,
|
||||||
|
is_admin,
|
||||||
|
Perform,
|
||||||
};
|
};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
234
src/lib.rs
234
src/lib.rs
|
@ -1,238 +1,4 @@
|
||||||
#![recursion_limit = "512"]
|
#![recursion_limit = "512"]
|
||||||
|
|
||||||
pub mod api;
|
|
||||||
pub mod code_migrations;
|
pub mod code_migrations;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod version;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use lemmy_db::DbPool;
|
|
||||||
use lemmy_utils::{
|
|
||||||
apub::get_apub_protocol_string,
|
|
||||||
request::{retry, RecvError},
|
|
||||||
settings::Settings,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use log::error;
|
|
||||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
|
||||||
use reqwest::Client;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct IframelyResponse {
|
|
||||||
title: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
thumbnail_url: Option<String>,
|
|
||||||
html: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_iframely(client: &Client, url: &str) -> Result<IframelyResponse, LemmyError> {
|
|
||||||
let fetch_url = format!("http://iframely/oembed?url={}", url);
|
|
||||||
|
|
||||||
let response = retry(|| client.get(&fetch_url).send()).await?;
|
|
||||||
|
|
||||||
let res: IframelyResponse = response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(|e| RecvError(e.to_string()))?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct PictrsResponse {
|
|
||||||
files: Vec<PictrsFile>,
|
|
||||||
msg: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct PictrsFile {
|
|
||||||
file: String,
|
|
||||||
delete_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResponse, LemmyError> {
|
|
||||||
is_image_content_type(client, image_url).await?;
|
|
||||||
|
|
||||||
let fetch_url = format!(
|
|
||||||
"http://pictrs:8080/image/download?url={}",
|
|
||||||
utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = retry(|| client.get(&fetch_url).send()).await?;
|
|
||||||
|
|
||||||
let response: PictrsResponse = response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(|e| RecvError(e.to_string()))?;
|
|
||||||
|
|
||||||
if response.msg == "ok" {
|
|
||||||
Ok(response)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("{}", &response.msg).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_iframely_and_pictrs_data(
|
|
||||||
client: &Client,
|
|
||||||
url: Option<String>,
|
|
||||||
) -> (
|
|
||||||
Option<String>,
|
|
||||||
Option<String>,
|
|
||||||
Option<String>,
|
|
||||||
Option<String>,
|
|
||||||
) {
|
|
||||||
match &url {
|
|
||||||
Some(url) => {
|
|
||||||
// Fetch iframely data
|
|
||||||
let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
|
|
||||||
match fetch_iframely(client, url).await {
|
|
||||||
Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
|
|
||||||
Err(e) => {
|
|
||||||
error!("iframely err: {}", e);
|
|
||||||
(None, None, None, None)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch pictrs thumbnail
|
|
||||||
let pictrs_hash = match iframely_thumbnail_url {
|
|
||||||
Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
|
|
||||||
Ok(res) => Some(res.files[0].file.to_owned()),
|
|
||||||
Err(e) => {
|
|
||||||
error!("pictrs err: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Try to generate a small thumbnail if iframely is not supported
|
|
||||||
None => match fetch_pictrs(client, &url).await {
|
|
||||||
Ok(res) => Some(res.files[0].file.to_owned()),
|
|
||||||
Err(e) => {
|
|
||||||
error!("pictrs err: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// The full urls are necessary for federation
|
|
||||||
let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
|
|
||||||
Some(format!(
|
|
||||||
"{}://{}/pictrs/image/{}",
|
|
||||||
get_apub_protocol_string(),
|
|
||||||
Settings::get().hostname,
|
|
||||||
pictrs_hash
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
iframely_title,
|
|
||||||
iframely_description,
|
|
||||||
iframely_html,
|
|
||||||
pictrs_thumbnail,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => (None, None, None, None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
|
|
||||||
let response = retry(|| client.get(test).send()).await?;
|
|
||||||
|
|
||||||
if response
|
|
||||||
.headers()
|
|
||||||
.get("Content-Type")
|
|
||||||
.ok_or_else(|| anyhow!("No Content-Type header"))?
|
|
||||||
.to_str()?
|
|
||||||
.starts_with("image/")
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Not an image type.").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
|
|
||||||
let mut built_text = String::new();
|
|
||||||
|
|
||||||
// Building proper speech text for espeak
|
|
||||||
for mut c in captcha.chars() {
|
|
||||||
let new_str = if c.is_alphabetic() {
|
|
||||||
if c.is_lowercase() {
|
|
||||||
c.make_ascii_uppercase();
|
|
||||||
format!("lower case {} ... ", c)
|
|
||||||
} else {
|
|
||||||
c.make_ascii_uppercase();
|
|
||||||
format!("capital {} ... ", c)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{} ...", c)
|
|
||||||
};
|
|
||||||
|
|
||||||
built_text.push_str(&new_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
espeak_wav_base64(&built_text)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
|
|
||||||
// Make a temp file path
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
|
|
||||||
|
|
||||||
// Write the wav file
|
|
||||||
Command::new("espeak")
|
|
||||||
.arg("-w")
|
|
||||||
.arg(&file_path)
|
|
||||||
.arg(text)
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
// Read the wav file bytes
|
|
||||||
let bytes = std::fs::read(&file_path)?;
|
|
||||||
|
|
||||||
// Delete the file
|
|
||||||
std::fs::remove_file(file_path)?;
|
|
||||||
|
|
||||||
// Convert to base64
|
|
||||||
let base64 = base64::encode(bytes);
|
|
||||||
|
|
||||||
Ok(base64)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{captcha_espeak_wav_base64, is_image_content_type};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_image() {
|
|
||||||
actix_rt::System::new("tset_image").block_on(async move {
|
|
||||||
let client = reqwest::Client::default();
|
|
||||||
assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
|
|
||||||
assert!(is_image_content_type(&client,
|
|
||||||
"https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
|
|
||||||
)
|
|
||||||
.await.is_err()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_espeak() {
|
|
||||||
assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
// These helped with testing
|
|
||||||
// #[test]
|
|
||||||
// fn test_iframely() {
|
|
||||||
// let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
|
|
||||||
// assert!(res.is_ok());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn test_pictshare() {
|
|
||||||
// let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
|
|
||||||
// assert!(res.is_ok());
|
|
||||||
// let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
|
|
||||||
// assert!(res_other.is_err());
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,14 +16,11 @@ use diesel::{
|
||||||
PgConnection,
|
PgConnection,
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use lemmy_api::match_websocket_operation;
|
||||||
use lemmy_apub::activity_queue::create_activity_queue;
|
use lemmy_apub::activity_queue::create_activity_queue;
|
||||||
use lemmy_db::get_database_url_from_env;
|
use lemmy_db::get_database_url_from_env;
|
||||||
use lemmy_rate_limit::{rate_limiter::RateLimiter, RateLimit};
|
use lemmy_rate_limit::{rate_limiter::RateLimiter, RateLimit};
|
||||||
use lemmy_server::{
|
use lemmy_server::{code_migrations::run_advanced_migrations, routes::*};
|
||||||
api::match_websocket_operation,
|
|
||||||
code_migrations::run_advanced_migrations,
|
|
||||||
routes::*,
|
|
||||||
};
|
|
||||||
use lemmy_structs::blocking;
|
use lemmy_structs::blocking;
|
||||||
use lemmy_utils::{settings::Settings, LemmyError, CACHE_CONTROL_REGEX};
|
use lemmy_utils::{settings::Settings, LemmyError, CACHE_CONTROL_REGEX};
|
||||||
use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
|
use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::api::Perform;
|
|
||||||
use actix_web::{error::ErrorBadRequest, *};
|
use actix_web::{error::ErrorBadRequest, *};
|
||||||
|
use lemmy_api::Perform;
|
||||||
use lemmy_rate_limit::RateLimit;
|
use lemmy_rate_limit::RateLimit;
|
||||||
use lemmy_structs::{comment::*, community::*, post::*, site::*, user::*};
|
use lemmy_structs::{comment::*, community::*, post::*, site::*, user::*};
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::api::claims::Claims;
|
|
||||||
use actix_web::{error::ErrorBadRequest, *};
|
use actix_web::{error::ErrorBadRequest, *};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use diesel::PgConnection;
|
use diesel::PgConnection;
|
||||||
|
use lemmy_api::claims::Claims;
|
||||||
use lemmy_db::{
|
use lemmy_db::{
|
||||||
comment_view::{ReplyQueryBuilder, ReplyView},
|
comment_view::{ReplyQueryBuilder, ReplyView},
|
||||||
community::Community,
|
community::Community,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::version;
|
|
||||||
use actix_web::{body::Body, error::ErrorBadRequest, *};
|
use actix_web::{body::Body, error::ErrorBadRequest, *};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use lemmy_api::version;
|
||||||
use lemmy_db::site_view::SiteView;
|
use lemmy_db::site_view::SiteView;
|
||||||
use lemmy_structs::blocking;
|
use lemmy_structs::blocking;
|
||||||
use lemmy_utils::{apub::get_apub_protocol_string, settings::Settings, LemmyError};
|
use lemmy_utils::{apub::get_apub_protocol_string, settings::Settings, LemmyError};
|
||||||
|
|
Loading…
Reference in a new issue