atagen

macltok

public
Star0
Watch0
cbb715e119632f60c9b3ec1bc8803f1f33e39dce
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/engine/url_preview.rs

0291516d40b5atagen2026-10-22 08:31 UTC1
use std::{
0291516d40b5atagen2026-10-22 08:31 UTC2
    collections::{HashMap, VecDeque},
0291516d40b5atagen2026-10-22 08:31 UTC3
    sync::{
0291516d40b5atagen2026-10-22 08:31 UTC4
        Arc,
0291516d40b5atagen2026-10-22 08:31 UTC5
        atomic::{AtomicBool, Ordering},
0291516d40b5atagen2026-10-22 08:31 UTC6
    },
0291516d40b5atagen2026-10-22 08:31 UTC7
    time::Duration,
0291516d40b5atagen2026-10-22 08:31 UTC8
};
0291516d40b5atagen2026-10-22 08:31 UTC9
0291516d40b5atagen2026-10-22 08:31 UTC10
use futures_util::{FutureExt, future::WeakShared};
0291516d40b5atagen2026-10-22 08:31 UTC11
use matrix_sdk::{
0291516d40b5atagen2026-10-22 08:31 UTC12
    Client, HttpError,
0291516d40b5atagen2026-10-22 08:31 UTC13
    ruma::api::{
0291516d40b5atagen2026-10-22 08:31 UTC14
        Metadata,
0291516d40b5atagen2026-10-22 08:31 UTC15
        client::{authenticated_media, media},
0291516d40b5atagen2026-10-22 08:31 UTC16
    },
0291516d40b5atagen2026-10-22 08:31 UTC17
};
0291516d40b5atagen2026-10-22 08:31 UTC18
use ruma::{
0291516d40b5atagen2026-10-22 08:31 UTC19
    api::error::ErrorKind as ApiErrorKind,
0291516d40b5atagen2026-10-22 08:31 UTC20
    serde::{JsonObject, Raw},
0291516d40b5atagen2026-10-22 08:31 UTC21
    time::Instant,
0291516d40b5atagen2026-10-22 08:31 UTC22
};
0291516d40b5atagen2026-10-22 08:31 UTC23
use tokio::sync::Mutex;
0291516d40b5atagen2026-10-22 08:31 UTC24
0291516d40b5atagen2026-10-22 08:31 UTC25
use super::executor::EngineBoxFuture;
0291516d40b5atagen2026-10-22 08:31 UTC26
use crate::{Error, LinkPreview, normalise::server_preview};
0291516d40b5atagen2026-10-22 08:31 UTC27
0291516d40b5atagen2026-10-22 08:31 UTC28
const HIT_FOR: Duration = Duration::from_secs(60 * 60);
0291516d40b5atagen2026-10-22 08:31 UTC29
const MISS_FOR: Duration = Duration::from_secs(10 * 60);
0291516d40b5atagen2026-10-22 08:31 UTC30
const REMEMBERED: usize = 512;
0291516d40b5atagen2026-10-22 08:31 UTC31
0291516d40b5atagen2026-10-22 08:31 UTC32
type Lookup = EngineBoxFuture<'static, Result<Option<LinkPreview>, Error>>;
0291516d40b5atagen2026-10-22 08:31 UTC33
0291516d40b5atagen2026-10-22 08:31 UTC34
/// the homeserver rate limits `preview_url` and a busy timeline repeats links, so answers are
0291516d40b5atagen2026-10-22 08:31 UTC35
/// kept a while, misses too, and asks for one url at once share one request
0291516d40b5atagen2026-10-22 08:31 UTC36
pub(crate) struct UrlPreviews(Arc<Inner>);
0291516d40b5atagen2026-10-22 08:31 UTC37
0291516d40b5atagen2026-10-22 08:31 UTC38
struct Inner {
0291516d40b5atagen2026-10-22 08:31 UTC39
    client: Client,
0291516d40b5atagen2026-10-22 08:31 UTC40
    /// a server that doesn't preview at all says so once and isn't asked again
0291516d40b5atagen2026-10-22 08:31 UTC41
    unsupported: AtomicBool,
0291516d40b5atagen2026-10-22 08:31 UTC42
    cache: Mutex<Cache>,
0291516d40b5atagen2026-10-22 08:31 UTC43
    lookups: Mutex<HashMap<String, WeakShared<Lookup>>>,
0291516d40b5atagen2026-10-22 08:31 UTC44
}
0291516d40b5atagen2026-10-22 08:31 UTC45
0291516d40b5atagen2026-10-22 08:31 UTC46
#[derive(Default)]
0291516d40b5atagen2026-10-22 08:31 UTC47
struct Cache {
0291516d40b5atagen2026-10-22 08:31 UTC48
    entries: HashMap<String, (Instant, Option<LinkPreview>)>,
0291516d40b5atagen2026-10-22 08:31 UTC49
    order: VecDeque<String>,
0291516d40b5atagen2026-10-22 08:31 UTC50
}
0291516d40b5atagen2026-10-22 08:31 UTC51
0291516d40b5atagen2026-10-22 08:31 UTC52
impl UrlPreviews {
0291516d40b5atagen2026-10-22 08:31 UTC53
    pub(crate) fn new(client: Client) -> Self {
0291516d40b5atagen2026-10-22 08:31 UTC54
        Self(Arc::new(Inner {
0291516d40b5atagen2026-10-22 08:31 UTC55
            client,
0291516d40b5atagen2026-10-22 08:31 UTC56
            unsupported: AtomicBool::new(false),
0291516d40b5atagen2026-10-22 08:31 UTC57
            cache: Mutex::new(Cache::default()),
0291516d40b5atagen2026-10-22 08:31 UTC58
            lookups: Mutex::new(HashMap::new()),
0291516d40b5atagen2026-10-22 08:31 UTC59
        }))
0291516d40b5atagen2026-10-22 08:31 UTC60
    }
0291516d40b5atagen2026-10-22 08:31 UTC61
0291516d40b5atagen2026-10-22 08:31 UTC62
    pub(crate) async fn preview(&self, url: &str) -> Result<Option<LinkPreview>, Error> {
0291516d40b5atagen2026-10-22 08:31 UTC63
        if self.0.unsupported.load(Ordering::Relaxed) {
0291516d40b5atagen2026-10-22 08:31 UTC64
            return Ok(None);
0291516d40b5atagen2026-10-22 08:31 UTC65
        }
0291516d40b5atagen2026-10-22 08:31 UTC66
        if let Some(preview) = self.0.cache.lock().await.get(url) {
0291516d40b5atagen2026-10-22 08:31 UTC67
            return Ok(preview);
0291516d40b5atagen2026-10-22 08:31 UTC68
        }
0291516d40b5atagen2026-10-22 08:31 UTC69
        let lookup = {
0291516d40b5atagen2026-10-22 08:31 UTC70
            let mut lookups = self.0.lookups.lock().await;
0291516d40b5atagen2026-10-22 08:31 UTC71
            lookups.retain(|_, lookup| lookup.upgrade().is_some());
0291516d40b5atagen2026-10-22 08:31 UTC72
            match lookups.get(url).and_then(WeakShared::upgrade) {
0291516d40b5atagen2026-10-22 08:31 UTC73
                Some(lookup) => lookup,
0291516d40b5atagen2026-10-22 08:31 UTC74
                None => {
0291516d40b5atagen2026-10-22 08:31 UTC75
                    let inner = self.0.clone();
0291516d40b5atagen2026-10-22 08:31 UTC76
                    let owned = url.to_owned();
0291516d40b5atagen2026-10-22 08:31 UTC77
                    let lookup: Lookup = Box::pin(async move { inner.look_up(owned).await });
0291516d40b5atagen2026-10-22 08:31 UTC78
                    let lookup = lookup.shared();
0291516d40b5atagen2026-10-22 08:31 UTC79
                    if let Some(weak) = lookup.downgrade() {
0291516d40b5atagen2026-10-22 08:31 UTC80
                        lookups.insert(url.to_owned(), weak);
0291516d40b5atagen2026-10-22 08:31 UTC81
                    }
0291516d40b5atagen2026-10-22 08:31 UTC82
                    lookup
0291516d40b5atagen2026-10-22 08:31 UTC83
                }
0291516d40b5atagen2026-10-22 08:31 UTC84
            }
0291516d40b5atagen2026-10-22 08:31 UTC85
        };
0291516d40b5atagen2026-10-22 08:31 UTC86
        lookup.await
0291516d40b5atagen2026-10-22 08:31 UTC87
    }
0291516d40b5atagen2026-10-22 08:31 UTC88
}
0291516d40b5atagen2026-10-22 08:31 UTC89
0291516d40b5atagen2026-10-22 08:31 UTC90
impl Inner {
0291516d40b5atagen2026-10-22 08:31 UTC91
    async fn look_up(&self, url: String) -> Result<Option<LinkPreview>, Error> {
0291516d40b5atagen2026-10-22 08:31 UTC92
        let data = match self.fetch(&url).await {
0291516d40b5atagen2026-10-22 08:31 UTC93
            Ok(data) => data,
0291516d40b5atagen2026-10-22 08:31 UTC94
            Err(error) => {
0291516d40b5atagen2026-10-22 08:31 UTC95
                let Some(api) = error.as_client_api_error() else {
0291516d40b5atagen2026-10-22 08:31 UTC96
                    return Err(Error::from_sdk(
0291516d40b5atagen2026-10-22 08:31 UTC97
                        "asking for a url preview failed",
0291516d40b5atagen2026-10-22 08:31 UTC98
                        error.into(),
0291516d40b5atagen2026-10-22 08:31 UTC99
                    ));
0291516d40b5atagen2026-10-22 08:31 UTC100
                };
0291516d40b5atagen2026-10-22 08:31 UTC101
                let unrecognised = matches!(api.error_kind(), Some(ApiErrorKind::Unrecognized))
0291516d40b5atagen2026-10-22 08:31 UTC102
                    || api.status_code.as_u16() == 405;
0291516d40b5atagen2026-10-22 08:31 UTC103
                if unrecognised {
0291516d40b5atagen2026-10-22 08:31 UTC104
                    self.unsupported.store(true, Ordering::Relaxed);
0291516d40b5atagen2026-10-22 08:31 UTC105
                    return Ok(None);
0291516d40b5atagen2026-10-22 08:31 UTC106
                }
0291516d40b5atagen2026-10-22 08:31 UTC107
                // synapse answers 500 for a page it can't download, 502 when dns fails and 403 for
0291516d40b5atagen2026-10-22 08:31 UTC108
                // its blocklist (synapse/media/url_previewer.py). a real fault hides in the 500s too,
0291516d40b5atagen2026-10-22 08:31 UTC109
                // but a missing card is cheap and only cached ten minutes
0291516d40b5atagen2026-10-22 08:31 UTC110
                if !matches!(api.status_code.as_u16(), 400 | 403 | 404 | 500 | 502) {
0291516d40b5atagen2026-10-22 08:31 UTC111
                    return Err(Error::from_sdk(
0291516d40b5atagen2026-10-22 08:31 UTC112
                        "asking for a url preview failed",
0291516d40b5atagen2026-10-22 08:31 UTC113
                        error.into(),
0291516d40b5atagen2026-10-22 08:31 UTC114
                    ));
0291516d40b5atagen2026-10-22 08:31 UTC115
                }
0291516d40b5atagen2026-10-22 08:31 UTC116
                None
0291516d40b5atagen2026-10-22 08:31 UTC117
            }
0291516d40b5atagen2026-10-22 08:31 UTC118
        };
0291516d40b5atagen2026-10-22 08:31 UTC119
        let preview = data.and_then(|data| server_preview(&url, &data));
0291516d40b5atagen2026-10-22 08:31 UTC120
        self.cache.lock().await.insert(url, preview.clone());
0291516d40b5atagen2026-10-22 08:31 UTC121
        Ok(preview)
0291516d40b5atagen2026-10-22 08:31 UTC122
    }
0291516d40b5atagen2026-10-22 08:31 UTC123
0291516d40b5atagen2026-10-22 08:31 UTC124
    async fn fetch(&self, url: &str) -> Result<Option<Raw<JsonObject>>, HttpError> {
0291516d40b5atagen2026-10-22 08:31 UTC125
        let versions = self.client.supported_versions().await?;
0291516d40b5atagen2026-10-22 08:31 UTC126
        let data = if authenticated_media::get_media_preview::v1::Request::PATH_BUILDER
0291516d40b5atagen2026-10-22 08:31 UTC127
            .is_supported(&versions)
0291516d40b5atagen2026-10-22 08:31 UTC128
        {
0291516d40b5atagen2026-10-22 08:31 UTC129
            self.client
0291516d40b5atagen2026-10-22 08:31 UTC130
                .send(authenticated_media::get_media_preview::v1::Request::new(
0291516d40b5atagen2026-10-22 08:31 UTC131
                    url.to_owned(),
0291516d40b5atagen2026-10-22 08:31 UTC132
                ))
0291516d40b5atagen2026-10-22 08:31 UTC133
                .await?
0291516d40b5atagen2026-10-22 08:31 UTC134
                .data
0291516d40b5atagen2026-10-22 08:31 UTC135
        } else {
0291516d40b5atagen2026-10-22 08:31 UTC136
            #[allow(deprecated)]
0291516d40b5atagen2026-10-22 08:31 UTC137
            let request = media::get_media_preview::v3::Request::new(url.to_owned());
0291516d40b5atagen2026-10-22 08:31 UTC138
            self.client.send(request).await?.data
0291516d40b5atagen2026-10-22 08:31 UTC139
        };
0291516d40b5atagen2026-10-22 08:31 UTC140
        Ok(data.map(Raw::from_json))
0291516d40b5atagen2026-10-22 08:31 UTC141
    }
0291516d40b5atagen2026-10-22 08:31 UTC142
}
0291516d40b5atagen2026-10-22 08:31 UTC143
0291516d40b5atagen2026-10-22 08:31 UTC144
impl Cache {
0291516d40b5atagen2026-10-22 08:31 UTC145
    fn get(&mut self, url: &str) -> Option<Option<LinkPreview>> {
0291516d40b5atagen2026-10-22 08:31 UTC146
        let (at, preview) = self.entries.get(url)?;
0291516d40b5atagen2026-10-22 08:31 UTC147
        let lasts = if preview.is_some() { HIT_FOR } else { MISS_FOR };
0291516d40b5atagen2026-10-22 08:31 UTC148
        (at.elapsed() < lasts).then(|| preview.clone())
0291516d40b5atagen2026-10-22 08:31 UTC149
    }
0291516d40b5atagen2026-10-22 08:31 UTC150
0291516d40b5atagen2026-10-22 08:31 UTC151
    fn insert(&mut self, url: String, preview: Option<LinkPreview>) {
0291516d40b5atagen2026-10-22 08:31 UTC152
        if self
0291516d40b5atagen2026-10-22 08:31 UTC153
            .entries
0291516d40b5atagen2026-10-22 08:31 UTC154
            .insert(url.clone(), (Instant::now(), preview))
0291516d40b5atagen2026-10-22 08:31 UTC155
            .is_none()
0291516d40b5atagen2026-10-22 08:31 UTC156
        {
0291516d40b5atagen2026-10-22 08:31 UTC157
            self.order.push_back(url);
0291516d40b5atagen2026-10-22 08:31 UTC158
        }
0291516d40b5atagen2026-10-22 08:31 UTC159
        while self.entries.len() > REMEMBERED {
0291516d40b5atagen2026-10-22 08:31 UTC160
            let Some(oldest) = self.order.pop_front() else {
0291516d40b5atagen2026-10-22 08:31 UTC161
                break;
0291516d40b5atagen2026-10-22 08:31 UTC162
            };
0291516d40b5atagen2026-10-22 08:31 UTC163
            self.entries.remove(&oldest);
0291516d40b5atagen2026-10-22 08:31 UTC164
        }
0291516d40b5atagen2026-10-22 08:31 UTC165
    }
0291516d40b5atagen2026-10-22 08:31 UTC166
}