use jacquard_common::types::string::{Did, Handle};
use serde_json::{Map, Value, json};
use unicode_segmentation::UnicodeSegmentation as _;
const STATS: [&str; 7] = [
"merged-pull-request-count",
"closed-pull-request-count",
"open-pull-request-count",
"open-issue-count",
"closed-issue-count",
"repository-count",
"star-count",
];
const MAX_URI_BYTES: usize = 8192;
const MAX_LABELS: usize = 10;
const MAX_LABEL_BYTES: usize = 128;
/// the org.tangled.actor.profile fields within the lexicon's limits. free text is cut to fit,
/// anything else out of shape is dropped. the avatar is the knot's own copy, so it isn't kept
pub(crate) fn sanitized(value: &Value) -> Map<String, Value> {
let fields = [
("description", text(&value["description"], 2560, 256)),
("location", text(&value["location"], 400, 40)),
("pronouns", text(&value["pronouns"], 200, 30)),
("links", list(&value["links"], 5, is_uri)),
(
"stats",
list(&value["stats"], 2, |stat| STATS.contains(&stat)),
),
(
"pinnedRepositories",
list(&value["pinnedRepositories"], 6, |did| Did::new(did).is_ok()),
),
(
"isOrganization",
value["isOrganization"].as_bool().map(Value::Bool),
),
(
"preferredHandle",
value["preferredHandle"]
.as_str()
.filter(|handle| Handle::new(*handle).is_ok())
.map(Value::from),
),
("labels", labels(&value["labels"])),
];
fields
.into_iter()
.filter_map(|(field, kept)| Some((field.to_string(), kept?)))
.collect()
}
fn text(value: &Value, max_bytes: usize, max_graphemes: usize) -> Option<Value> {
let text = value.as_str()?;
let end = text
.grapheme_indices(true)
.take(max_graphemes)
.map(|(start, grapheme)| start + grapheme.len())
.take_while(|&end| end <= max_bytes)
.last()
.unwrap_or(0);
Some(text[..end].into())
}
fn list(value: &Value, max: usize, valid: impl Fn(&str) -> bool) -> Option<Value> {
let items = value.as_array()?.iter().filter_map(Value::as_str);
Some(items.filter(|item| valid(item)).take(max).collect())
}
fn is_uri(uri: &str) -> bool {
uri.len() <= MAX_URI_BYTES && url::Url::parse(uri).is_ok()
}
fn labels(value: &Value) -> Option<Value> {
let values: Vec<Value> = value["values"]
.as_array()?
.iter()
.filter_map(|label| label["val"].as_str())
.filter(|val| val.len() <= MAX_LABEL_BYTES)
.take(MAX_LABELS)
.map(|val| json!({ "val": val }))
.collect();
Some(json!({ "$type": "com.atproto.label.defs#selfLabels", "values": values }))
}