2018-10-31 10:40:20 +01:00
|
|
|
use activitypub::object::Image;
|
2018-11-24 12:44:17 +01:00
|
|
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
2018-10-31 10:40:20 +01:00
|
|
|
use guid_create::GUID;
|
|
|
|
use reqwest;
|
2018-09-02 22:55:42 +02:00
|
|
|
use serde_json;
|
2018-10-31 10:40:20 +01:00
|
|
|
use std::{fs, path::Path};
|
|
|
|
|
|
|
|
use plume_common::activity_pub::Id;
|
2018-09-02 22:55:42 +02:00
|
|
|
|
|
|
|
use instance::Instance;
|
2018-09-02 13:34:48 +02:00
|
|
|
use schema::medias;
|
2018-11-24 12:44:17 +01:00
|
|
|
use users::User;
|
|
|
|
use {ap_url, Connection};
|
2018-09-02 13:34:48 +02:00
|
|
|
|
2018-09-27 23:06:40 +02:00
|
|
|
#[derive(Clone, Identifiable, Queryable, Serialize)]
|
2018-09-02 13:34:48 +02:00
|
|
|
pub struct Media {
|
|
|
|
pub id: i32,
|
|
|
|
pub file_path: String,
|
|
|
|
pub alt_text: String,
|
|
|
|
pub is_remote: bool,
|
|
|
|
pub remote_url: Option<String>,
|
|
|
|
pub sensitive: bool,
|
2018-09-02 22:55:42 +02:00
|
|
|
pub content_warning: Option<String>,
|
2018-11-24 12:44:17 +01:00
|
|
|
pub owner_id: i32,
|
2018-09-02 13:34:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Insertable)]
|
|
|
|
#[table_name = "medias"]
|
|
|
|
pub struct NewMedia {
|
|
|
|
pub file_path: String,
|
|
|
|
pub alt_text: String,
|
|
|
|
pub is_remote: bool,
|
|
|
|
pub remote_url: Option<String>,
|
|
|
|
pub sensitive: bool,
|
2018-09-02 22:55:42 +02:00
|
|
|
pub content_warning: Option<String>,
|
2018-11-24 12:44:17 +01:00
|
|
|
pub owner_id: i32,
|
2018-09-02 13:34:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Media {
|
|
|
|
insert!(medias, NewMedia);
|
|
|
|
get!(medias);
|
2018-09-02 22:55:42 +02:00
|
|
|
list_by!(medias, for_user, owner_id as i32);
|
|
|
|
|
2018-11-24 12:44:17 +01:00
|
|
|
pub fn list_all_medias(conn: &Connection) -> Vec<Media> {
|
|
|
|
medias::table
|
|
|
|
.load::<Media>(conn)
|
|
|
|
.expect("Media::list_all_medias: loading error")
|
|
|
|
}
|
|
|
|
|
2018-09-26 17:22:42 +02:00
|
|
|
pub fn to_json(&self, conn: &Connection) -> serde_json::Value {
|
2018-10-20 08:44:33 +02:00
|
|
|
let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error");
|
2018-09-03 12:16:07 +02:00
|
|
|
let url = self.url(conn);
|
2018-11-24 12:44:17 +01:00
|
|
|
let (cat, preview, html, md) = match self
|
|
|
|
.file_path
|
|
|
|
.rsplitn(2, '.')
|
|
|
|
.next()
|
|
|
|
.expect("Media::to_json: extension error")
|
|
|
|
{
|
2018-09-03 11:22:14 +02:00
|
|
|
"png" | "jpg" | "jpeg" | "gif" | "svg" => (
|
2018-10-31 14:40:31 +01:00
|
|
|
"image",
|
2018-11-24 12:44:17 +01:00
|
|
|
format!(
|
|
|
|
"<img src=\"{}\" alt=\"{}\" title=\"{}\" class=\"preview\">",
|
|
|
|
url, self.alt_text, self.alt_text
|
|
|
|
),
|
|
|
|
format!(
|
|
|
|
"<img src=\"{}\" alt=\"{}\" title=\"{}\">",
|
|
|
|
url, self.alt_text, self.alt_text
|
|
|
|
),
|
2018-09-03 12:16:07 +02:00
|
|
|
format!("![{}]({})", self.alt_text, url),
|
2018-09-02 22:55:42 +02:00
|
|
|
),
|
|
|
|
"mp3" | "wav" | "flac" => (
|
2018-10-31 14:40:31 +01:00
|
|
|
"audio",
|
2018-11-24 12:44:17 +01:00
|
|
|
format!(
|
|
|
|
"<audio src=\"{}\" title=\"{}\" class=\"preview\"></audio>",
|
|
|
|
url, self.alt_text
|
|
|
|
),
|
|
|
|
format!(
|
|
|
|
"<audio src=\"{}\" title=\"{}\"></audio>",
|
|
|
|
url, self.alt_text
|
|
|
|
),
|
|
|
|
format!(
|
|
|
|
"<audio src=\"{}\" title=\"{}\"></audio>",
|
|
|
|
url, self.alt_text
|
|
|
|
),
|
2018-09-02 22:55:42 +02:00
|
|
|
),
|
|
|
|
"mp4" | "avi" | "webm" | "mov" => (
|
2018-10-31 14:40:31 +01:00
|
|
|
"video",
|
2018-11-24 12:44:17 +01:00
|
|
|
format!(
|
|
|
|
"<video src=\"{}\" title=\"{}\" class=\"preview\"></video>",
|
|
|
|
url, self.alt_text
|
|
|
|
),
|
|
|
|
format!(
|
|
|
|
"<video src=\"{}\" title=\"{}\"></video>",
|
|
|
|
url, self.alt_text
|
|
|
|
),
|
|
|
|
format!(
|
|
|
|
"<video src=\"{}\" title=\"{}\"></video>",
|
|
|
|
url, self.alt_text
|
|
|
|
),
|
2018-09-02 22:55:42 +02:00
|
|
|
),
|
2018-11-24 12:44:17 +01:00
|
|
|
_ => ("unknown", String::new(), String::new(), String::new()),
|
2018-09-02 22:55:42 +02:00
|
|
|
};
|
|
|
|
json["html_preview"] = json!(preview);
|
|
|
|
json["html"] = json!(html);
|
2018-09-03 12:16:07 +02:00
|
|
|
json["url"] = json!(url);
|
|
|
|
json["md"] = json!(md);
|
2018-10-31 14:40:31 +01:00
|
|
|
json["category"] = json!(cat);
|
2018-09-02 22:55:42 +02:00
|
|
|
json
|
|
|
|
}
|
|
|
|
|
2018-09-26 17:22:42 +02:00
|
|
|
pub fn url(&self, conn: &Connection) -> String {
|
2018-09-03 13:17:59 +02:00
|
|
|
if self.is_remote {
|
2018-11-26 10:21:52 +01:00
|
|
|
self.remote_url.clone().unwrap_or_default()
|
2018-09-03 13:17:59 +02:00
|
|
|
} else {
|
2018-11-26 10:21:52 +01:00
|
|
|
ap_url(&format!(
|
2018-11-24 12:44:17 +01:00
|
|
|
"{}/{}",
|
|
|
|
Instance::get_local(conn)
|
|
|
|
.expect("Media::url: local instance not found error")
|
|
|
|
.public_domain,
|
|
|
|
self.file_path
|
|
|
|
))
|
2018-09-03 13:17:59 +02:00
|
|
|
}
|
2018-09-02 22:55:42 +02:00
|
|
|
}
|
2018-09-02 23:10:15 +02:00
|
|
|
|
2018-09-26 17:22:42 +02:00
|
|
|
pub fn delete(&self, conn: &Connection) {
|
2018-11-24 12:44:17 +01:00
|
|
|
if !self.is_remote {
|
|
|
|
fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error");
|
|
|
|
}
|
|
|
|
diesel::delete(self)
|
|
|
|
.execute(conn)
|
|
|
|
.expect("Media::delete: database entry deletion error");
|
2018-09-02 23:10:15 +02:00
|
|
|
}
|
2018-09-03 13:17:59 +02:00
|
|
|
|
2018-12-02 19:07:36 +01:00
|
|
|
pub fn save_remote(conn: &Connection, url: String, user: &User) -> Result<Media, ()> {
|
|
|
|
if url.contains(&['<', '>', '"'][..]) {
|
|
|
|
Err(())
|
|
|
|
} else {
|
|
|
|
Ok(Media::insert(
|
|
|
|
conn,
|
|
|
|
NewMedia {
|
|
|
|
file_path: String::new(),
|
|
|
|
alt_text: String::new(),
|
|
|
|
is_remote: true,
|
|
|
|
remote_url: Some(url),
|
|
|
|
sensitive: false,
|
|
|
|
content_warning: None,
|
|
|
|
owner_id: user.id,
|
|
|
|
},
|
|
|
|
))
|
|
|
|
}
|
2018-09-03 13:17:59 +02:00
|
|
|
}
|
|
|
|
|
2018-11-24 12:44:17 +01:00
|
|
|
pub fn set_owner(&self, conn: &Connection, user: &User) {
|
2018-09-03 13:17:59 +02:00
|
|
|
diesel::update(self)
|
2018-11-24 12:44:17 +01:00
|
|
|
.set(medias::owner_id.eq(user.id))
|
2018-09-03 13:17:59 +02:00
|
|
|
.execute(conn)
|
2018-10-20 08:44:33 +02:00
|
|
|
.expect("Media::set_owner: owner update error");
|
2018-09-03 13:17:59 +02:00
|
|
|
}
|
2018-10-31 10:40:20 +01:00
|
|
|
|
|
|
|
// TODO: merge with save_remote?
|
2018-11-26 10:21:52 +01:00
|
|
|
pub fn from_activity(conn: &Connection, image: &Image) -> Option<Media> {
|
2018-10-31 10:40:20 +01:00
|
|
|
let remote_url = image.object_props.url_string().ok()?;
|
2018-11-24 12:44:17 +01:00
|
|
|
let ext = remote_url
|
|
|
|
.rsplit('.')
|
|
|
|
.next()
|
|
|
|
.map(|ext| ext.to_owned())
|
2018-11-26 10:21:52 +01:00
|
|
|
.unwrap_or_else(|| String::from("png"));
|
2018-11-24 12:44:17 +01:00
|
|
|
let path =
|
|
|
|
Path::new("static")
|
|
|
|
.join("media")
|
|
|
|
.join(format!("{}.{}", GUID::rand().to_string(), ext));
|
2018-10-31 10:40:20 +01:00
|
|
|
|
|
|
|
let mut dest = fs::File::create(path.clone()).ok()?;
|
2018-11-24 12:44:17 +01:00
|
|
|
reqwest::get(remote_url.as_str())
|
|
|
|
.ok()?
|
|
|
|
.copy_to(&mut dest)
|
|
|
|
.ok()?;
|
|
|
|
|
|
|
|
Some(Media::insert(
|
|
|
|
conn,
|
|
|
|
NewMedia {
|
|
|
|
file_path: path.to_str()?.to_string(),
|
|
|
|
alt_text: image.object_props.content_string().ok()?,
|
2018-12-02 19:07:36 +01:00
|
|
|
is_remote: false,
|
2018-11-24 12:44:17 +01:00
|
|
|
remote_url: None,
|
|
|
|
sensitive: image.object_props.summary_string().is_ok(),
|
|
|
|
content_warning: image.object_props.summary_string().ok(),
|
|
|
|
owner_id: User::from_url(
|
|
|
|
conn,
|
|
|
|
image
|
|
|
|
.object_props
|
|
|
|
.attributed_to_link_vec::<Id>()
|
|
|
|
.ok()?
|
|
|
|
.into_iter()
|
|
|
|
.next()?
|
2018-11-26 10:21:52 +01:00
|
|
|
.as_ref(),
|
2018-11-24 12:44:17 +01:00
|
|
|
)?.id,
|
|
|
|
},
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
pub(crate) mod tests {
|
|
|
|
use super::*;
|
|
|
|
use diesel::Connection;
|
|
|
|
use std::env::{current_dir, set_current_dir};
|
|
|
|
use std::fs;
|
|
|
|
use std::path::Path;
|
|
|
|
use tests::db;
|
|
|
|
use users::tests as usersTests;
|
|
|
|
use Connection as Conn;
|
|
|
|
|
|
|
|
pub(crate) fn fill_database(conn: &Conn) -> Vec<Media> {
|
|
|
|
let mut wd = current_dir().unwrap().to_path_buf();
|
|
|
|
while wd.pop() {
|
|
|
|
if wd.join(".git").exists() {
|
|
|
|
set_current_dir(wd).unwrap();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let users = usersTests::fill_database(conn);
|
|
|
|
let user_one = users[0].id;
|
|
|
|
let user_two = users[1].id;
|
|
|
|
let f1 = "static/media/1.png".to_owned();
|
|
|
|
let f2 = "static/media/2.mp3".to_owned();
|
|
|
|
fs::write(f1.clone(), []).unwrap();
|
|
|
|
fs::write(f2.clone(), []).unwrap();
|
|
|
|
vec![
|
|
|
|
NewMedia {
|
|
|
|
file_path: f1,
|
|
|
|
alt_text: "some alt".to_owned(),
|
|
|
|
is_remote: false,
|
|
|
|
remote_url: None,
|
|
|
|
sensitive: false,
|
|
|
|
content_warning: None,
|
|
|
|
owner_id: user_one,
|
|
|
|
},
|
|
|
|
NewMedia {
|
|
|
|
file_path: f2,
|
|
|
|
alt_text: "alt message".to_owned(),
|
|
|
|
is_remote: false,
|
|
|
|
remote_url: None,
|
|
|
|
sensitive: true,
|
|
|
|
content_warning: Some("Content warning".to_owned()),
|
|
|
|
owner_id: user_one,
|
|
|
|
},
|
|
|
|
NewMedia {
|
|
|
|
file_path: "".to_owned(),
|
|
|
|
alt_text: "another alt".to_owned(),
|
|
|
|
is_remote: true,
|
|
|
|
remote_url: Some("https://example.com/".to_owned()),
|
|
|
|
sensitive: false,
|
|
|
|
content_warning: None,
|
|
|
|
owner_id: user_two,
|
|
|
|
},
|
|
|
|
].into_iter()
|
|
|
|
.map(|nm| Media::insert(conn, nm))
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn clean(conn: &Conn) {
|
|
|
|
//used to remove files generated by tests
|
|
|
|
for media in Media::list_all_medias(conn) {
|
|
|
|
media.delete(conn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//set_owner
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn delete() {
|
|
|
|
let conn = &db();
|
|
|
|
conn.test_transaction::<_, (), _>(|| {
|
|
|
|
let user = usersTests::fill_database(conn)[0].id;
|
|
|
|
fill_database(conn);
|
|
|
|
|
|
|
|
let path = "static/media/test_deletion".to_owned();
|
|
|
|
fs::write(path.clone(), []).unwrap();
|
|
|
|
|
|
|
|
let media = Media::insert(
|
|
|
|
conn,
|
|
|
|
NewMedia {
|
|
|
|
file_path: path.clone(),
|
|
|
|
alt_text: "alt message".to_owned(),
|
|
|
|
is_remote: false,
|
|
|
|
remote_url: None,
|
|
|
|
sensitive: false,
|
|
|
|
content_warning: None,
|
|
|
|
owner_id: user,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
assert!(Path::new(&path).exists());
|
|
|
|
media.delete(conn);
|
|
|
|
assert!(!Path::new(&path).exists());
|
|
|
|
|
|
|
|
clean(conn);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn set_owner() {
|
|
|
|
let conn = &db();
|
|
|
|
conn.test_transaction::<_, (), _>(|| {
|
|
|
|
let users = usersTests::fill_database(conn);
|
|
|
|
let u1 = &users[0];
|
|
|
|
let u2 = &users[1];
|
|
|
|
fill_database(conn);
|
|
|
|
|
|
|
|
let path = "static/media/test_set_owner".to_owned();
|
|
|
|
fs::write(path.clone(), []).unwrap();
|
|
|
|
|
|
|
|
let media = Media::insert(
|
|
|
|
conn,
|
|
|
|
NewMedia {
|
|
|
|
file_path: path.clone(),
|
|
|
|
alt_text: "alt message".to_owned(),
|
|
|
|
is_remote: false,
|
|
|
|
remote_url: None,
|
|
|
|
sensitive: false,
|
|
|
|
content_warning: None,
|
|
|
|
owner_id: u1.id,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
Media::for_user(conn, u1.id)
|
|
|
|
.iter()
|
|
|
|
.any(|m| m.id == media.id)
|
|
|
|
);
|
|
|
|
assert!(
|
|
|
|
!Media::for_user(conn, u2.id)
|
|
|
|
.iter()
|
|
|
|
.any(|m| m.id == media.id)
|
|
|
|
);
|
|
|
|
media.set_owner(conn, u2);
|
|
|
|
assert!(
|
|
|
|
!Media::for_user(conn, u1.id)
|
|
|
|
.iter()
|
|
|
|
.any(|m| m.id == media.id)
|
|
|
|
);
|
|
|
|
assert!(
|
|
|
|
Media::for_user(conn, u2.id)
|
|
|
|
.iter()
|
|
|
|
.any(|m| m.id == media.id)
|
|
|
|
);
|
|
|
|
|
|
|
|
clean(conn);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
});
|
2018-10-31 10:40:20 +01:00
|
|
|
}
|
2018-09-02 13:34:48 +02:00
|
|
|
}
|