mirror of
https://gitlab.com/famedly/conduit.git
synced 2025-06-27 16:35:59 +00:00
parent
dc0fa09a57
commit
bb4cade9fd
12 changed files with 460 additions and 3 deletions
|
@ -147,6 +147,8 @@ tikv-jemallocator = { version = "0.5.0", features = [
|
||||||
|
|
||||||
sd-notify = { version = "0.4.1", optional = true }
|
sd-notify = { version = "0.4.1", optional = true }
|
||||||
|
|
||||||
|
webpage = { version = "1.6", default-features = false, optional = true }
|
||||||
|
|
||||||
# Used for matrix spec type definitions and helpers
|
# Used for matrix spec type definitions and helpers
|
||||||
[dependencies.ruma]
|
[dependencies.ruma]
|
||||||
features = [
|
features = [
|
||||||
|
@ -186,6 +188,7 @@ conduit_bin = ["axum"]
|
||||||
jemalloc = ["tikv-jemallocator"]
|
jemalloc = ["tikv-jemallocator"]
|
||||||
sqlite = ["parking_lot", "rusqlite", "tokio/signal"]
|
sqlite = ["parking_lot", "rusqlite", "tokio/signal"]
|
||||||
systemd = ["sd-notify"]
|
systemd = ["sd-notify"]
|
||||||
|
url_preview = ["webpage"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "conduit"
|
name = "conduit"
|
||||||
|
|
|
@ -47,6 +47,9 @@ registration_token = ""
|
||||||
allow_check_for_updates = true
|
allow_check_for_updates = true
|
||||||
allow_federation = true
|
allow_federation = true
|
||||||
|
|
||||||
|
# Allows clients to request a URL preview
|
||||||
|
allow_url_preview = false
|
||||||
|
|
||||||
# Enable the display name lightning bolt on registration.
|
# Enable the display name lightning bolt on registration.
|
||||||
enable_lightning_bolt = true
|
enable_lightning_bolt = true
|
||||||
|
|
||||||
|
|
3
debian/postinst
vendored
3
debian/postinst
vendored
|
@ -84,6 +84,9 @@ allow_check_for_updates = true
|
||||||
# Enable the display name lightning bolt on registration.
|
# Enable the display name lightning bolt on registration.
|
||||||
enable_lightning_bolt = true
|
enable_lightning_bolt = true
|
||||||
|
|
||||||
|
# Allows clients to request a URL preview
|
||||||
|
allow_url_preview = false
|
||||||
|
|
||||||
# Servers listed here will be used to gather public keys of other servers.
|
# Servers listed here will be used to gather public keys of other servers.
|
||||||
# Generally, copying this exactly should be enough. (Currently, Conduit doesn't
|
# Generally, copying this exactly should be enough. (Currently, Conduit doesn't
|
||||||
# support batched key requests, so this list should only contain Synapse
|
# support batched key requests, so this list should only contain Synapse
|
||||||
|
|
|
@ -2,13 +2,22 @@ use std::time::Duration;
|
||||||
|
|
||||||
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
|
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
|
||||||
use ruma::api::client::{
|
use ruma::api::client::{
|
||||||
error::ErrorKind,
|
error::{ErrorKind, RetryAfter},
|
||||||
media::{
|
media::{
|
||||||
create_content, get_content, get_content_as_filename, get_content_thumbnail,
|
create_content, get_content, get_content_as_filename, get_content_thumbnail,
|
||||||
get_media_config,
|
get_media_config, get_media_preview
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
use {
|
||||||
|
crate::service::media::UrlPreviewData,
|
||||||
|
webpage::HTML,
|
||||||
|
std::{io::Cursor, net::IpAddr, sync::Arc, time::Duration},
|
||||||
|
tokio::sync::Notify,
|
||||||
|
image::io::Reader as ImgReader,
|
||||||
|
};
|
||||||
|
|
||||||
const MXC_LENGTH: usize = 32;
|
const MXC_LENGTH: usize = 32;
|
||||||
|
|
||||||
/// # `GET /_matrix/media/r0/config`
|
/// # `GET /_matrix/media/r0/config`
|
||||||
|
@ -22,6 +31,230 @@ pub async fn get_media_config_route(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
async fn download_image(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<UrlPreviewData> {
|
||||||
|
let image = client.get(url).send().await?.bytes().await?;
|
||||||
|
let mxc = format!(
|
||||||
|
"mxc://{}/{}",
|
||||||
|
services().globals.server_name(),
|
||||||
|
utils::random_string(MXC_LENGTH)
|
||||||
|
);
|
||||||
|
services().media
|
||||||
|
.create(mxc.clone(), None, None, &image)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (width, height) = match ImgReader::new(Cursor::new(&image)).with_guessed_format() {
|
||||||
|
Err(_) => (None, None),
|
||||||
|
Ok(reader) => match reader.into_dimensions() {
|
||||||
|
Err(_) => (None, None),
|
||||||
|
Ok((width, height)) => (Some(width), Some(height)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(UrlPreviewData {
|
||||||
|
image: Some(mxc),
|
||||||
|
image_size: Some(image.len()),
|
||||||
|
image_width: width,
|
||||||
|
image_height: height,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
async fn download_html(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<UrlPreviewData> {
|
||||||
|
let max_download_size = 300_000;
|
||||||
|
|
||||||
|
let mut response = client.get(url).send().await?;
|
||||||
|
|
||||||
|
let mut bytes: Vec<u8> = Vec::new();
|
||||||
|
while let Some(chunk) = response.chunk().await? {
|
||||||
|
bytes.extend_from_slice(&chunk);
|
||||||
|
if bytes.len() > max_download_size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let body = String::from_utf8_lossy(&bytes);
|
||||||
|
let html = match HTML::from_string(body.to_string(), Some(url.to_owned())) {
|
||||||
|
Ok(html) => html,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"Failed to parse HTML",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut data = match html.opengraph.images.first() {
|
||||||
|
None => UrlPreviewData::default(),
|
||||||
|
Some(obj) => download_image(client, &obj.url).await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let props = html.opengraph.properties;
|
||||||
|
/* use OpenGraph title/description, but fall back to HTML if not available */
|
||||||
|
data.title = props.get("title").cloned().or(html.title);
|
||||||
|
data.description = props.get("description").cloned().or(html.description);
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
fn url_request_allowed(addr: &IpAddr) -> bool {
|
||||||
|
// could be implemented with reqwest when it supports IP filtering:
|
||||||
|
// https://github.com/seanmonstar/reqwest/issues/1515
|
||||||
|
|
||||||
|
// TODO: simplify to .is_global() when it has been stabilized
|
||||||
|
match addr {
|
||||||
|
IpAddr::V4(ip4) => {
|
||||||
|
!(ip4.is_private()
|
||||||
|
|| ip4.is_loopback()
|
||||||
|
|| ip4.is_link_local()
|
||||||
|
|| ip4.is_multicast()
|
||||||
|
|| ip4.is_broadcast()
|
||||||
|
|| ip4.is_documentation()
|
||||||
|
|| ip4.is_unspecified())
|
||||||
|
}
|
||||||
|
IpAddr::V6(ip6) => !(ip6.is_loopback() || ip6.is_multicast() || ip6.is_unspecified()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
async fn request_url_preview(url: String) -> Result<UrlPreviewData> {
|
||||||
|
let client = services().globals.default_client();
|
||||||
|
let response = client.head(&url).send().await?;
|
||||||
|
|
||||||
|
if !response
|
||||||
|
.remote_addr()
|
||||||
|
.map_or(false, |a| url_request_allowed(&a.ip()))
|
||||||
|
{
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"Requesting from this address forbidden",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_type = match response
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
|
.and_then(|x| x.to_str().ok())
|
||||||
|
{
|
||||||
|
Some(ct) => ct,
|
||||||
|
None => {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"Unknown Content-Type",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = match content_type {
|
||||||
|
html if html.starts_with("text/html") => download_html(&client, &url).await?,
|
||||||
|
img if img.starts_with("image/") => download_image(&client, &url).await?,
|
||||||
|
_ => {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"Unsupported Content-Type",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
services().media.set_url_preview(&url, &data).await?;
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
async fn get_url_preview(url: String) -> Result<UrlPreviewData> {
|
||||||
|
if let Some(preview) = services().media.get_url_preview(&url).await {
|
||||||
|
return Ok(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
let notif_opt = services()
|
||||||
|
.media
|
||||||
|
.url_preview_requests
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&url)
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
match notif_opt {
|
||||||
|
None => {
|
||||||
|
let notifier = Arc::new(Notify::new());
|
||||||
|
{
|
||||||
|
services().media
|
||||||
|
.url_preview_requests
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert(url.clone(), notifier.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = request_url_preview(url.clone()).await;
|
||||||
|
|
||||||
|
notifier.notify_waiters();
|
||||||
|
|
||||||
|
{
|
||||||
|
services().media.url_preview_requests.write().unwrap().remove(&url);
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
Some(notifier) => {
|
||||||
|
// wait until being notified that request is finished
|
||||||
|
let notifier = notifier.clone();
|
||||||
|
let notifier = notifier.notified();
|
||||||
|
notifier.await;
|
||||||
|
|
||||||
|
services().media
|
||||||
|
.get_url_preview(&url)
|
||||||
|
.await
|
||||||
|
.ok_or(Error::BadRequest(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"No Preview available",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/media/r0/preview_url`
|
||||||
|
///
|
||||||
|
/// Returns URL preview.
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
pub async fn get_media_preview_route(
|
||||||
|
body: Ruma<get_media_preview::v3::Request>,
|
||||||
|
) -> Result<get_media_preview::v3::Response> {
|
||||||
|
if !services().globals.allow_url_preview() {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"Previewing URL not allowed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(preview) = get_url_preview(body.url.clone()).await {
|
||||||
|
let res = serde_json::value::to_raw_value(&preview).expect("Converting to JSON failed");
|
||||||
|
return Ok(get_media_preview::v3::Response::from_raw_value(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::BadRequest(
|
||||||
|
ErrorKind::LimitExceeded {
|
||||||
|
retry_after: Some(RetryAfter::Delay(Duration::from_secs(5))),
|
||||||
|
},
|
||||||
|
"Retry later",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "url_preview"))]
|
||||||
|
pub async fn get_media_preview_route(
|
||||||
|
_body: Ruma<get_media_preview::v3::Request>,
|
||||||
|
) -> Result<get_media_preview::v3::Response> {
|
||||||
|
Err(Error::BadRequest(
|
||||||
|
ErrorKind::Forbidden,
|
||||||
|
"URL preview not implemented",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// # `POST /_matrix/media/r0/upload`
|
/// # `POST /_matrix/media/r0/upload`
|
||||||
///
|
///
|
||||||
/// Permanently save media in the server.
|
/// Permanently save media in the server.
|
||||||
|
|
|
@ -53,6 +53,8 @@ pub struct Config {
|
||||||
pub allow_encryption: bool,
|
pub allow_encryption: bool,
|
||||||
#[serde(default = "false_fn")]
|
#[serde(default = "false_fn")]
|
||||||
pub allow_federation: bool,
|
pub allow_federation: bool,
|
||||||
|
#[serde(default = "false_fn")]
|
||||||
|
pub allow_url_preview: bool,
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
pub allow_room_creation: bool,
|
pub allow_room_creation: bool,
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
|
@ -184,6 +186,7 @@ impl fmt::Display for Config {
|
||||||
),
|
),
|
||||||
("Allow encryption", &self.allow_encryption.to_string()),
|
("Allow encryption", &self.allow_encryption.to_string()),
|
||||||
("Allow federation", &self.allow_federation.to_string()),
|
("Allow federation", &self.allow_federation.to_string()),
|
||||||
|
("Allow URL preview", &self.allow_url_preview.to_string()),
|
||||||
("Allow room creation", &self.allow_room_creation.to_string()),
|
("Allow room creation", &self.allow_room_creation.to_string()),
|
||||||
(
|
(
|
||||||
"JWT secret",
|
"JWT secret",
|
||||||
|
|
|
@ -2,6 +2,9 @@ use ruma::api::client::error::ErrorKind;
|
||||||
|
|
||||||
use crate::{database::KeyValueDatabase, service, utils, Error, Result};
|
use crate::{database::KeyValueDatabase, service, utils, Error, Result};
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
use crate::service::media::UrlPreviewData;
|
||||||
|
|
||||||
impl service::media::Data for KeyValueDatabase {
|
impl service::media::Data for KeyValueDatabase {
|
||||||
fn create_file_metadata(
|
fn create_file_metadata(
|
||||||
&self,
|
&self,
|
||||||
|
@ -79,4 +82,110 @@ impl service::media::Data for KeyValueDatabase {
|
||||||
};
|
};
|
||||||
Ok((content_disposition, content_type, key))
|
Ok((content_disposition, content_type, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
fn remove_url_preview(&self, url: &str) -> Result<()> {
|
||||||
|
self.url_previews.remove(url.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
fn set_url_preview(&self, url: &str, data: &UrlPreviewData, timestamp: std::time::Duration) -> Result<()> {
|
||||||
|
let mut value = Vec::<u8>::new();
|
||||||
|
value.extend_from_slice(×tamp.as_secs().to_be_bytes());
|
||||||
|
value.push(0xff);
|
||||||
|
value.extend_from_slice(
|
||||||
|
data.title
|
||||||
|
.as_ref()
|
||||||
|
.map(|t| t.as_bytes())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
value.push(0xff);
|
||||||
|
value.extend_from_slice(
|
||||||
|
data.description
|
||||||
|
.as_ref()
|
||||||
|
.map(|d| d.as_bytes())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
value.push(0xff);
|
||||||
|
value.extend_from_slice(
|
||||||
|
data.image
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.as_bytes())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
value.push(0xff);
|
||||||
|
value.extend_from_slice(&data.image_size.unwrap_or(0).to_be_bytes());
|
||||||
|
value.push(0xff);
|
||||||
|
value.extend_from_slice(&data.image_width.unwrap_or(0).to_be_bytes());
|
||||||
|
value.push(0xff);
|
||||||
|
value.extend_from_slice(&data.image_height.unwrap_or(0).to_be_bytes());
|
||||||
|
|
||||||
|
self.url_previews.insert(url.as_bytes(), &value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
fn get_url_preview(&self, url: &str) -> Option<UrlPreviewData> {
|
||||||
|
let values = self.url_previews.get(url.as_bytes()).ok()??;
|
||||||
|
|
||||||
|
let mut values = values.split(|&b| b == 0xff);
|
||||||
|
|
||||||
|
let _ts = match values
|
||||||
|
.next()
|
||||||
|
.map(|b| u64::from_be_bytes(b.try_into().expect("valid BE array")))
|
||||||
|
{
|
||||||
|
Some(0) => None,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let title = match values
|
||||||
|
.next()
|
||||||
|
.and_then(|b| String::from_utf8(b.to_vec()).ok())
|
||||||
|
{
|
||||||
|
Some(s) if s.is_empty() => None,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let description = match values
|
||||||
|
.next()
|
||||||
|
.and_then(|b| String::from_utf8(b.to_vec()).ok())
|
||||||
|
{
|
||||||
|
Some(s) if s.is_empty() => None,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let image = match values
|
||||||
|
.next()
|
||||||
|
.and_then(|b| String::from_utf8(b.to_vec()).ok())
|
||||||
|
{
|
||||||
|
Some(s) if s.is_empty() => None,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let image_size = match values
|
||||||
|
.next()
|
||||||
|
.map(|b| usize::from_be_bytes(b.try_into().expect("valid BE array")))
|
||||||
|
{
|
||||||
|
Some(0) => None,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let image_width = match values
|
||||||
|
.next()
|
||||||
|
.map(|b| u32::from_be_bytes(b.try_into().expect("valid BE array")))
|
||||||
|
{
|
||||||
|
Some(0) => None,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let image_height = match values
|
||||||
|
.next()
|
||||||
|
.map(|b| u32::from_be_bytes(b.try_into().expect("valid BE array")))
|
||||||
|
{
|
||||||
|
Some(0) => None,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(UrlPreviewData {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
image,
|
||||||
|
image_size,
|
||||||
|
image_width,
|
||||||
|
image_height,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,8 @@ pub struct KeyValueDatabase {
|
||||||
|
|
||||||
//pub media: media::Media,
|
//pub media: media::Media,
|
||||||
pub(super) mediaid_file: Arc<dyn KvTree>, // MediaId = MXC + WidthHeight + ContentDisposition + ContentType
|
pub(super) mediaid_file: Arc<dyn KvTree>, // MediaId = MXC + WidthHeight + ContentDisposition + ContentType
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
pub(super) url_previews: Arc<dyn KvTree>,
|
||||||
//pub key_backups: key_backups::KeyBackups,
|
//pub key_backups: key_backups::KeyBackups,
|
||||||
pub(super) backupid_algorithm: Arc<dyn KvTree>, // BackupId = UserId + Version(Count)
|
pub(super) backupid_algorithm: Arc<dyn KvTree>, // BackupId = UserId + Version(Count)
|
||||||
pub(super) backupid_etag: Arc<dyn KvTree>, // BackupId = UserId + Version(Count)
|
pub(super) backupid_etag: Arc<dyn KvTree>, // BackupId = UserId + Version(Count)
|
||||||
|
@ -362,6 +364,8 @@ impl KeyValueDatabase {
|
||||||
roomuserdataid_accountdata: builder.open_tree("roomuserdataid_accountdata")?,
|
roomuserdataid_accountdata: builder.open_tree("roomuserdataid_accountdata")?,
|
||||||
roomusertype_roomuserdataid: builder.open_tree("roomusertype_roomuserdataid")?,
|
roomusertype_roomuserdataid: builder.open_tree("roomusertype_roomuserdataid")?,
|
||||||
mediaid_file: builder.open_tree("mediaid_file")?,
|
mediaid_file: builder.open_tree("mediaid_file")?,
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
url_previews: builder.open_tree("url_previews")?,
|
||||||
backupid_algorithm: builder.open_tree("backupid_algorithm")?,
|
backupid_algorithm: builder.open_tree("backupid_algorithm")?,
|
||||||
backupid_etag: builder.open_tree("backupid_etag")?,
|
backupid_etag: builder.open_tree("backupid_etag")?,
|
||||||
backupkeyid_backup: builder.open_tree("backupkeyid_backup")?,
|
backupkeyid_backup: builder.open_tree("backupkeyid_backup")?,
|
||||||
|
|
|
@ -379,6 +379,7 @@ fn routes(config: &Config) -> Router {
|
||||||
.ruma_route(client_server::turn_server_route)
|
.ruma_route(client_server::turn_server_route)
|
||||||
.ruma_route(client_server::send_event_to_device_route)
|
.ruma_route(client_server::send_event_to_device_route)
|
||||||
.ruma_route(client_server::get_media_config_route)
|
.ruma_route(client_server::get_media_config_route)
|
||||||
|
.ruma_route(client_server::get_media_preview_route)
|
||||||
.ruma_route(client_server::create_content_route)
|
.ruma_route(client_server::create_content_route)
|
||||||
.ruma_route(client_server::get_content_route)
|
.ruma_route(client_server::get_content_route)
|
||||||
.ruma_route(client_server::get_content_as_filename_route)
|
.ruma_route(client_server::get_content_as_filename_route)
|
||||||
|
|
|
@ -324,6 +324,10 @@ impl Service {
|
||||||
self.config.allow_federation
|
self.config.allow_federation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn allow_url_preview(&self) -> bool {
|
||||||
|
self.config.allow_url_preview
|
||||||
|
}
|
||||||
|
|
||||||
pub fn allow_room_creation(&self) -> bool {
|
pub fn allow_room_creation(&self) -> bool {
|
||||||
self.config.allow_room_creation
|
self.config.allow_room_creation
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,24 @@ pub trait Data: Send + Sync {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
) -> Result<(Option<String>, Option<String>, Vec<u8>)>;
|
) -> Result<(Option<String>, Option<String>, Vec<u8>)>;
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
fn remove_url_preview(
|
||||||
|
&self,
|
||||||
|
url: &str
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
fn set_url_preview(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
data: &super::UrlPreviewData,
|
||||||
|
timestamp: std::time::Duration,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
fn get_url_preview(
|
||||||
|
&self,
|
||||||
|
url: &str
|
||||||
|
) -> Option<super::UrlPreviewData>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,62 @@ use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
use {
|
||||||
|
std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
},
|
||||||
|
serde::Serialize,
|
||||||
|
std::time::SystemTime,
|
||||||
|
tokio::sync::Notify,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct FileMeta {
|
pub struct FileMeta {
|
||||||
pub content_disposition: Option<String>,
|
pub content_disposition: Option<String>,
|
||||||
pub content_type: Option<String>,
|
pub content_type: Option<String>,
|
||||||
pub file: Vec<u8>,
|
pub file: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
#[derive(Serialize, Default)]
|
||||||
|
pub struct UrlPreviewData {
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
rename(serialize = "og:title")
|
||||||
|
)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
rename(serialize = "og:description")
|
||||||
|
)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
rename(serialize = "og:image")
|
||||||
|
)]
|
||||||
|
pub image: Option<String>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
rename(serialize = "matrix:image:size")
|
||||||
|
)]
|
||||||
|
pub image_size: Option<usize>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
rename(serialize = "og:image:width")
|
||||||
|
)]
|
||||||
|
pub image_width: Option<u32>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
rename(serialize = "og:image:height")
|
||||||
|
)]
|
||||||
|
pub image_height: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
pub db: &'static dyn Data,
|
pub db: &'static dyn Data,
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
pub url_preview_requests: RwLock<HashMap<String, Arc<Notify>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Service {
|
impl Service {
|
||||||
|
@ -225,4 +273,23 @@ impl Service {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
pub async fn get_url_preview(&self, url: &str) -> Option<UrlPreviewData> {
|
||||||
|
self.db.get_url_preview(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
pub async fn remove_url_preview(&self, url: &str) -> Result<()> {
|
||||||
|
// TODO: also remove the downloaded image
|
||||||
|
self.db.remove_url_preview(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<()> {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.expect("valid system time");
|
||||||
|
self.db.set_url_preview(url, data, now)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ use std::{
|
||||||
sync::{Arc, Mutex as StdMutex},
|
sync::{Arc, Mutex as StdMutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use lru_cache::LruCache;
|
use lru_cache::LruCache;
|
||||||
use tokio::sync::{broadcast, Mutex};
|
use tokio::sync::{broadcast, Mutex};
|
||||||
|
|
||||||
|
@ -118,7 +121,11 @@ impl Services {
|
||||||
account_data: account_data::Service { db },
|
account_data: account_data::Service { db },
|
||||||
admin: admin::Service::build(),
|
admin: admin::Service::build(),
|
||||||
key_backups: key_backups::Service { db },
|
key_backups: key_backups::Service { db },
|
||||||
media: media::Service { db },
|
media: media::Service {
|
||||||
|
db,
|
||||||
|
#[cfg(feature = "url_preview")]
|
||||||
|
url_preview_requests: RwLock::new(HashMap::new())
|
||||||
|
},
|
||||||
sending: sending::Service::build(db, &config),
|
sending: sending::Service::build(db, &config),
|
||||||
|
|
||||||
globals: globals::Service::load(db, config)?,
|
globals: globals::Service::load(db, config)?,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue