upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/git_events.rs133
1 files changed, 120 insertions, 13 deletions
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs
index a5aef12..b512e44 100644
--- a/src/lib/git_events.rs
+++ b/src/lib/git_events.rs
@@ -978,30 +978,31 @@ pub fn is_event_proposal_root_for_branch(
978 )) 978 ))
979} 979}
980 980
981/// Compute the effective set of labels for `event`. 981/// Process hashtag labels (`#t` namespace) from a pre-fetched set of kind-1985
982/// events.
982/// 983///
983/// Labels come from two sources, both subject to the same permission check: 984/// Labels come from two sources, both subject to the same permission check:
984/// 985///
985/// 1. `t` tags on the event itself (self-reported by the event author). 986/// 1. `t` tags on the event itself (self-reported by the event author).
986/// 2. NIP-32 kind-1985 label events in `all_label_events` that reference 987/// 2. NIP-32 kind-1985 label events in `label_events` that reference `event`
987/// `event` via a lowercase `e` tag and carry `["L", "#t"]` + 988/// via a lowercase `e` tag and carry `["L", "#t"]` +
988/// `["l", "<value>", "#t"]` tags. 989/// `["l", "<value>", "#t"]` tags.
989/// 990///
990/// A label is only applied when the author of the source event (the original 991/// A label is only applied when the author of the source event is either the
991/// event for inline `t` tags, or the kind-1985 event for external labels) is 992/// author of `event` itself or one of the repository maintainers.
992/// either the author of `event` itself or one of the repository maintainers. 993///
993pub fn get_labels( 994/// Labels are additive — all valid label events contribute; there is no
995/// "latest wins" replacement semantics.
996pub fn process_labels(
994 event: &Event, 997 event: &Event,
995 repo_ref: &RepoRef, 998 repo_ref: &RepoRef,
996 all_label_events: &[Event], 999 label_events: &[Event],
997) -> Vec<String> { 1000) -> Vec<String> {
998 let is_permitted = |pubkey: &PublicKey| -> bool { 1001 let is_permitted = |pubkey: &PublicKey| -> bool {
999 pubkey.eq(&event.pubkey) || repo_ref.maintainers.contains(pubkey) 1002 pubkey.eq(&event.pubkey) || repo_ref.maintainers.contains(pubkey)
1000 }; 1003 };
1001 1004
1002 // 1. Inline `t` tags on the event itself — only if the event author is 1005 // 1. Inline `t` tags on the event itself.
1003 // permitted (they always are, since they authored the event, but we
1004 // keep the check symmetric with the external-label path).
1005 let mut labels: Vec<String> = if is_permitted(&event.pubkey) { 1006 let mut labels: Vec<String> = if is_permitted(&event.pubkey) {
1006 event 1007 event
1007 .tags 1008 .tags
@@ -1016,7 +1017,7 @@ pub fn get_labels(
1016 vec![] 1017 vec![]
1017 }; 1018 };
1018 1019
1019 // 2. External NIP-32 kind-1985 label events. 1020 // 2. External NIP-32 kind-1985 label events (`#t` namespace).
1020 // 1021 //
1021 // A valid label event must: 1022 // A valid label event must:
1022 // - be kind 1985 1023 // - be kind 1985
@@ -1025,7 +1026,7 @@ pub fn get_labels(
1025 // - have at least one `["l", "<value>", "#t"]` tag 1026 // - have at least one `["l", "<value>", "#t"]` tag
1026 // - be authored by a permitted pubkey 1027 // - be authored by a permitted pubkey
1027 let event_id_str = event.id.to_string(); 1028 let event_id_str = event.id.to_string();
1028 for label_event in all_label_events { 1029 for label_event in label_events {
1029 if !label_event.kind.eq(&KIND_LABEL) { 1030 if !label_event.kind.eq(&KIND_LABEL) {
1030 continue; 1031 continue;
1031 } 1032 }
@@ -1063,6 +1064,112 @@ pub fn get_labels(
1063 labels 1064 labels
1064} 1065}
1065 1066
1067/// Process the effective subject/title override for `event` from a pre-fetched
1068/// set of kind-1985 events.
1069///
1070/// Subject overrides use the `#subject` namespace:
1071/// `["L", "#subject"]` + `["l", "<new title>", "#subject"]`
1072///
1073/// Unlike hashtag labels, subject overrides are replaceable-style: only the
1074/// latest authorised event wins, with tiebreak by lexicographically larger
1075/// event ID (consistent with NIP-1 replaceable event semantics).
1076///
1077/// Only the author of `event` or a repository maintainer may set the subject.
1078/// Returns `None` when no valid subject override exists.
1079pub fn process_subject(
1080 event: &Event,
1081 repo_ref: &RepoRef,
1082 label_events: &[Event],
1083) -> Option<String> {
1084 let is_permitted = |pubkey: &PublicKey| -> bool {
1085 pubkey.eq(&event.pubkey) || repo_ref.maintainers.contains(pubkey)
1086 };
1087
1088 let event_id_str = event.id.to_string();
1089
1090 // Find the winning subject label event: latest created_at, tiebreak by
1091 // lexicographically larger event ID (NIP-1 replaceable event semantics).
1092 let winner = label_events
1093 .iter()
1094 .filter(|le| {
1095 if !le.kind.eq(&KIND_LABEL) {
1096 return false;
1097 }
1098 if !is_permitted(&le.pubkey) {
1099 return false;
1100 }
1101 // Must reference our event via a lowercase `e` tag.
1102 let references_event = le.tags.iter().any(|t| {
1103 let s = t.as_slice();
1104 s.len() >= 2 && s[0].eq("e") && s[1].eq(&event_id_str)
1105 });
1106 if !references_event {
1107 return false;
1108 }
1109 // Must declare the `#subject` namespace.
1110 let has_namespace = le.tags.iter().any(|t| {
1111 let s = t.as_slice();
1112 s.len() >= 2 && s[0].eq("L") && s[1].eq("#subject")
1113 });
1114 if !has_namespace {
1115 return false;
1116 }
1117 // Must have at least one non-empty `["l", "<value>", "#subject"]` tag.
1118 le.tags.iter().any(|t| {
1119 let s = t.as_slice();
1120 s.len() >= 3 && s[0].eq("l") && s[2].eq("#subject") && !s[1].is_empty()
1121 })
1122 })
1123 .max_by(|a, b| {
1124 // Primary: newer created_at wins.
1125 // Tiebreak: lexicographically larger event ID wins (NIP-1).
1126 a.created_at
1127 .cmp(&b.created_at)
1128 .then_with(|| a.id.to_string().cmp(&b.id.to_string()))
1129 })?;
1130
1131 // Extract the subject value from the winning event.
1132 winner.tags.iter().find_map(|t| {
1133 let s = t.as_slice();
1134 if s.len() >= 3 && s[0].eq("l") && s[2].eq("#subject") && !s[1].is_empty() {
1135 Some(s[1].clone())
1136 } else {
1137 None
1138 }
1139 })
1140}
1141
1142/// Compute both the effective hashtag labels and the subject/title override for
1143/// `event` from a pre-fetched set of kind-1985 events.
1144///
1145/// This is the primary entry point: callers should fetch label events once
1146/// (covering both `#t` and `#subject` namespaces) and pass them here to get
1147/// both results in a single pass.
1148///
1149/// Returns `(labels, subject_override)` where `subject_override` is `None`
1150/// when no authorised `#subject` label exists.
1151pub fn get_labels_and_subject(
1152 event: &Event,
1153 repo_ref: &RepoRef,
1154 label_events: &[Event],
1155) -> (Vec<String>, Option<String>) {
1156 (
1157 process_labels(event, repo_ref, label_events),
1158 process_subject(event, repo_ref, label_events),
1159 )
1160}
1161
1162/// Compatibility wrapper — returns only the hashtag labels.
1163///
1164/// Prefer [`get_labels_and_subject`] when the subject override is also needed.
1165pub fn get_labels(
1166 event: &Event,
1167 repo_ref: &RepoRef,
1168 label_events: &[Event],
1169) -> Vec<String> {
1170 process_labels(event, repo_ref, label_events)
1171}
1172
1066pub fn get_status( 1173pub fn get_status(
1067 proposal: &Event, 1174 proposal: &Event,
1068 repo_ref: &RepoRef, 1175 repo_ref: &RepoRef,