Compare commits
No commits in common. "b9a25bc3d7065196358f6ad62b643918d83393c0" and "cd67863a47d60d059a1ed97c9d0a61a85cdf8b53" have entirely different histories.
b9a25bc3d7
...
cd67863a47
10 changed files with 26 additions and 158 deletions
|
@ -22,7 +22,6 @@ openssl = "0.10" # TODO handle pubkeys with a smaller crate
|
|||
base64 = "0.22"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.8", features = ["v4"] }
|
||||
regex = "1.10"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_default = "0.1"
|
||||
|
@ -51,7 +50,7 @@ time = { version = "0.3", features = ["serde"], optional = true }
|
|||
async-recursion = "1.1"
|
||||
|
||||
[features]
|
||||
default = ["mastodon", "migrations", "cli"]
|
||||
default = ["migrations", "cli"]
|
||||
cli = []
|
||||
migrations = ["dep:sea-orm-migration"]
|
||||
mastodon = ["dep:mastodon-async-entities", "dep:time"]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use apb::{target::Addressed, Activity, ActivityMut, ActorMut, BaseMut, Node, Object, ObjectMut, PublicKeyMut};
|
||||
use reqwest::StatusCode;
|
||||
use sea_orm::{sea_query::Expr, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns, Set};
|
||||
use sea_orm::{sea_query::Expr, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set};
|
||||
|
||||
use crate::{errors::UpubError, model, routes::activitypub::jsonld::LD};
|
||||
|
||||
|
@ -14,37 +14,15 @@ impl apb::server::Outbox for Context {
|
|||
type Activity = serde_json::Value;
|
||||
|
||||
async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result<String> {
|
||||
let re = regex::Regex::new(r"@(\w+)@(\w+)").expect("failed compiling regex pattern");
|
||||
let raw_oid = uuid::Uuid::new_v4().to_string();
|
||||
let oid = self.oid(&raw_oid);
|
||||
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
|
||||
let activity_targets = object.addressed();
|
||||
|
||||
let mut content = object.content().map(|x| x.to_string());
|
||||
if let Some(c) = content {
|
||||
let mut tmp = mdhtml::safe_markdown(&c);
|
||||
for (full, [user, domain]) in re.captures_iter(&tmp.clone()).map(|x| x.extract()) {
|
||||
if let Ok(Some(uid)) = model::user::Entity::find()
|
||||
.filter(model::user::Column::PreferredUsername.eq(user))
|
||||
.filter(model::user::Column::Domain.eq(domain))
|
||||
.select_only()
|
||||
.select_column(model::user::Column::Id)
|
||||
.into_tuple::<String>()
|
||||
.one(self.db())
|
||||
.await
|
||||
{
|
||||
tmp = tmp.replacen(full, &format!("<a href=\"{uid}\" class=\"u-url mention\">@{user}</a>"), 1);
|
||||
}
|
||||
}
|
||||
content = Some(tmp);
|
||||
}
|
||||
|
||||
let object_model = self.insert_object(
|
||||
object
|
||||
.set_id(Some(&oid))
|
||||
.set_attributed_to(Node::link(uid.clone()))
|
||||
.set_published(Some(chrono::Utc::now()))
|
||||
.set_content(content.as_deref())
|
||||
.set_url(Node::maybe_link(self.cfg().instance.frontend.as_ref().map(|x| format!("{x}/objects/{raw_oid}")))),
|
||||
Some(self.domain().to_string()),
|
||||
).await?;
|
||||
|
|
|
@ -34,5 +34,3 @@ lazy_static = "1.4"
|
|||
chrono = { version = "0.4", features = ["serde"] }
|
||||
web-sys = { version = "0.3", features = ["Screen"] }
|
||||
mdhtml = { path = "../mdhtml/" }
|
||||
jrd = "0.1"
|
||||
tld = "2.35"
|
||||
|
|
|
@ -162,7 +162,7 @@
|
|||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
img.avatar {
|
||||
img.avatar-circle {
|
||||
display: inline;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
@ -170,14 +170,14 @@
|
|||
background-color: var(--background);
|
||||
border: .3em solid var(--accent);
|
||||
}
|
||||
img.inline {
|
||||
img.avatar-inline {
|
||||
display: inline;
|
||||
height: .75em;
|
||||
border-radius: 50%;
|
||||
}
|
||||
img.avatar-actor {
|
||||
min-height: 2em;
|
||||
max-height: 2em;
|
||||
min-width: 2em;
|
||||
max-width: 2em;
|
||||
img.inline-avatar {
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
}
|
||||
.box {
|
||||
border: 3px solid var(--accent);
|
||||
|
@ -309,7 +309,7 @@
|
|||
var(--background) 1em
|
||||
);
|
||||
}
|
||||
.spinner {
|
||||
.loader {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
span.dots {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use apb::{ActivityMut, Base, BaseMut, Object, ObjectMut};
|
||||
|
||||
use leptos::*;
|
||||
use leptos_use::DebounceOptions;
|
||||
use crate::{prelude::*, WEBFINGER};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ReplyControls {
|
||||
|
@ -35,34 +34,11 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
|||
let reply = use_context::<ReplyControls>().expect("missing reply controls");
|
||||
let (posting, set_posting) = create_signal(false);
|
||||
let (error, set_error) = create_signal(None);
|
||||
let (content, set_content) = create_signal("".to_string());
|
||||
let summary_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let content_ref: NodeRef<html::Textarea> = create_node_ref();
|
||||
let public_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let followers_ref: NodeRef<html::Input> = create_node_ref();
|
||||
let private_ref: NodeRef<html::Input> = create_node_ref();
|
||||
|
||||
// TODO is this too abusive with resources? im even checking if TLD exists...
|
||||
let mentions = create_local_resource(
|
||||
move || content.get(),
|
||||
move |c| async move {
|
||||
let mut out = Vec::new();
|
||||
for word in c.split(' ') {
|
||||
if !word.starts_with('@') { break };
|
||||
let stripped = word.replacen('@', "", 1);
|
||||
if let Some((user, domain)) = stripped.split_once('@') {
|
||||
if let Some(tld) = domain.split('.').last() {
|
||||
if tld::exist(tld) {
|
||||
if let Some(uid) = WEBFINGER.blocking_resolve(user, domain).await {
|
||||
out.push(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
},
|
||||
);
|
||||
|
||||
view! {
|
||||
<div>
|
||||
{move ||
|
||||
|
@ -75,22 +51,13 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
|||
on:click=move|_| reply.clear()
|
||||
title={format!("> {r} | ctx: {}", reply.context.get().unwrap_or_default())}
|
||||
>
|
||||
"✒️"
|
||||
"📨"
|
||||
</span>
|
||||
{actor_strip}
|
||||
<small class="tiny ml-1">"["<a class="clean" title="remove reply" href="#" on:click=move |_| reply.clear() >reply</a>"]"</small>
|
||||
<small>{actor_strip}</small>
|
||||
</span>
|
||||
}
|
||||
})
|
||||
}
|
||||
{move ||
|
||||
mentions.get()
|
||||
.map(|x| x.into_iter().map(|u| match CACHE.get(&u) {
|
||||
Some(u) => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><ActorStrip object=u /></span> }.into_view(),
|
||||
None => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><a href={Uri::web(U::User, &u)}>{u}</a></span> }.into_view(),
|
||||
})
|
||||
.collect_view())
|
||||
}
|
||||
<table class="align w-100">
|
||||
<tr>
|
||||
<td><input type="checkbox" on:input=move |ev| advanced.set(event_target_checked(&ev)) title="toggle advanced controls" /></td>
|
||||
|
@ -98,10 +65,7 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
<textarea rows="6" class="w-100" title="content" placeholder="\n look at nothing\n what do you see?"
|
||||
prop:value=content
|
||||
on:input=move |ev| set_content.set(event_target_value(&ev))
|
||||
></textarea>
|
||||
<textarea rows="6" class="w-100" node_ref=content_ref title="content" placeholder="\n look at nothing\n what do you see?" ></textarea>
|
||||
|
||||
<table class="align rev w-100">
|
||||
<tr>
|
||||
|
@ -112,7 +76,7 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
|||
set_posting.set(true);
|
||||
spawn_local(async move {
|
||||
let summary = get_if_some(summary_ref);
|
||||
let content = content.get();
|
||||
let content = content_ref.get().map(|x| x.value()).unwrap_or_default();
|
||||
let mut cc_vec = Vec::new();
|
||||
let mut to_vec = Vec::new();
|
||||
if get_checked(followers_ref) {
|
||||
|
@ -128,9 +92,6 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
|||
}
|
||||
}
|
||||
}
|
||||
for mention in mentions.get().as_deref().unwrap_or(&[]) {
|
||||
to_vec.push(mention.to_string());
|
||||
}
|
||||
let payload = serde_json::Value::Object(serde_json::Map::default())
|
||||
.set_object_type(Some(apb::ObjectType::Note))
|
||||
.set_summary(summary.as_deref())
|
||||
|
@ -144,7 +105,7 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
|
|||
Ok(()) => {
|
||||
set_error.set(None);
|
||||
if let Some(x) = summary_ref.get() { x.set_value("") }
|
||||
set_content.set("".to_string());
|
||||
if let Some(x) = content_ref.get() { x.set_value("") }
|
||||
},
|
||||
}
|
||||
set_posting.set(false);
|
||||
|
|
|
@ -11,7 +11,7 @@ pub fn ActorStrip(object: crate::Object) -> impl IntoView {
|
|||
let avatar = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into());
|
||||
view! {
|
||||
<a href={Uri::web(U::User, &actor_id)} class="clean hover">
|
||||
<img src={avatar} class="avatar inline mr-s" /><b>{username}</b><small>@{domain}</small>
|
||||
<img src={avatar} class="avatar-inline mr-s" /><b>{username}</b><small>@{domain}</small>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ pub fn ActorBanner(object: crate::Object) -> impl IntoView {
|
|||
<div>
|
||||
<table class="align" >
|
||||
<tr>
|
||||
<td rowspan="2" ><a href={uri.clone()} ><img class="avatar avatar-actor" src={avatar_url} /></a></td>
|
||||
<td rowspan="2" ><a href={uri.clone()} ><img class="avatar-circle inline-avatar" src={avatar_url} /></a></td>
|
||||
<td><b>{display_name}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -24,13 +24,12 @@ use uriproxy::UriClass;
|
|||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CACHE: ObjectCache = ObjectCache::default();
|
||||
pub static ref WEBFINGER: WebfingerCache = WebfingerCache::default();
|
||||
}
|
||||
|
||||
pub type Object = Arc<serde_json::Value>;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ObjectCache(Arc<dashmap::DashMap<String, Object>>);
|
||||
pub struct ObjectCache(pub Arc<dashmap::DashMap<String, Object>>);
|
||||
|
||||
impl ObjectCache {
|
||||
pub fn get(&self, k: &str) -> Option<Object> {
|
||||
|
@ -60,69 +59,6 @@ impl ObjectCache {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum LookupStatus {
|
||||
Resolving,
|
||||
Found(String),
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct WebfingerCache(Arc<dashmap::DashMap<String, LookupStatus>>);
|
||||
|
||||
impl WebfingerCache {
|
||||
pub async fn blocking_resolve(&self, user: &str, domain: &str) -> Option<String> {
|
||||
if let Some(x) = self.get(user, domain) { return Some(x); }
|
||||
self.fetch(user, domain).await;
|
||||
self.get(user, domain)
|
||||
}
|
||||
|
||||
pub fn resolve(&self, user: &str, domain: &str) -> Option<String> {
|
||||
if let Some(x) = self.get(user, domain) { return Some(x); }
|
||||
let (_self, user, domain) = (self.clone(), user.to_string(), domain.to_string());
|
||||
leptos::spawn_local(async move { _self.fetch(&user, &domain).await });
|
||||
None
|
||||
}
|
||||
|
||||
fn get(&self, user: &str, domain: &str) -> Option<String> {
|
||||
let query = format!("{user}@{domain}");
|
||||
match self.0.get(&query).map(|x| (*x).clone())? {
|
||||
LookupStatus::Resolving | LookupStatus::NotFound => None,
|
||||
LookupStatus::Found(x) => Some(x),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch(&self, user: &str, domain: &str) {
|
||||
let query = format!("{user}@{domain}");
|
||||
self.0.insert(query.to_string(), LookupStatus::Resolving);
|
||||
match reqwest::get(format!("{URL_BASE}/.well-known/webfinger?resource=acct:{query}")).await {
|
||||
Ok(res) => match res.error_for_status() {
|
||||
Ok(res) => match res.json::<jrd::JsonResourceDescriptor>().await {
|
||||
Ok(doc) => {
|
||||
if let Some(uid) = doc.links.into_iter().find(|x| x.rel == "self").map(|x| x.href).flatten() {
|
||||
self.0.insert(query, LookupStatus::Found(uid));
|
||||
} else {
|
||||
self.0.insert(query, LookupStatus::NotFound);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("invalid webfinger response: {e:?}");
|
||||
self.0.remove(&query);
|
||||
},
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("could not resolve webfinbger: {e:?}");
|
||||
self.0.insert(query, LookupStatus::NotFound);
|
||||
},
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("failed accessing webfinger server: {e:?}");
|
||||
self.0.remove(&query);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Http;
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@ pub fn ConfigPage(setter: WriteSignal<crate::Config>) -> impl IntoView {
|
|||
let (color, set_color) = leptos_use::use_css_var("--accent");
|
||||
let (_color_rgb, set_color_rgb) = leptos_use::use_css_var("--accent-rgb");
|
||||
|
||||
// TODO should this be responsive? idk
|
||||
let previous_color = config.get_untracked().accent_color;
|
||||
let previous_color = config.get().accent_color;
|
||||
set_color_rgb.set(parse_hex(&previous_color));
|
||||
set_color.set(previous_color);
|
||||
|
||||
|
@ -92,7 +91,7 @@ pub fn ConfigPage(setter: WriteSignal<crate::Config>) -> impl IntoView {
|
|||
<li><span title="objects without a related activity to display"><input type="checkbox" prop:checked=get_cfg!(filter orphans) on:input=set_cfg!(filter orphans) />" orphans"</span></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<p class="center"><a href="/web/config/dev" title="access the devtools page">devtools</a></p>
|
||||
<p><a href="/web/config/dev" title="access the devtools page">devtools</a></p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,15 +20,11 @@ pub fn DebugPage() -> impl IntoView {
|
|||
cached_query,
|
||||
move |(query, cached)| async move {
|
||||
set_text.set(query.clone());
|
||||
set_error.set(false);
|
||||
if query.is_empty() { return serde_json::Value::Null };
|
||||
if cached {
|
||||
match CACHE.get(&query) {
|
||||
Some(x) => (*x).clone(),
|
||||
None => {
|
||||
set_error.set(true);
|
||||
serde_json::Value::Null
|
||||
},
|
||||
None => serde_json::Value::Null,
|
||||
}
|
||||
} else {
|
||||
debug_fetch(&format!("{URL_BASE}/proxy?id={query}"), auth, set_error).await
|
||||
|
@ -60,7 +56,7 @@ pub fn DebugPage() -> impl IntoView {
|
|||
</td>
|
||||
<td>
|
||||
<input type="checkbox" title="load from local cache" value="cached"
|
||||
class:spinner=loading
|
||||
class:loader=loading
|
||||
prop:checked=cached
|
||||
on:input=move |ev| set_cached.set(event_target_checked(&ev))
|
||||
/>
|
||||
|
@ -102,6 +98,7 @@ pub fn DebugPage() -> impl IntoView {
|
|||
|
||||
// this is a rather weird way to fetch but i want to see the bare error text if it fails!
|
||||
async fn debug_fetch(url: &str, token: Auth, error: WriteSignal<bool>) -> serde_json::Value {
|
||||
error.set(false);
|
||||
match Http::request::<()>(reqwest::Method::GET, url, None, token).await {
|
||||
Ok(res) => {
|
||||
if res.error_for_status_ref().is_err() {
|
||||
|
|
|
@ -104,7 +104,7 @@ pub fn UserPage(tl: Timeline) -> impl IntoView {
|
|||
<table class="pl-2 pr-2 align w-100" style="table-layout: fixed">
|
||||
<tr>
|
||||
<td rowspan=4 style="width: 8em">
|
||||
<img class="avatar avatar-border mr-s" src={avatar_url} style="height: 7em; width: 7em"/>
|
||||
<img class="avatar-circle avatar-border mr-s" src={avatar_url} style="height: 7em; width: 7em"/>
|
||||
</td>
|
||||
<td rowspan=2 class="bottom">
|
||||
<b class="big">{display_name}</b>{actor_type_tag}
|
||||
|
|
Loading…
Reference in a new issue