hive-forge: show + post/remove emoji reactions on issues, PRs, and comments

This commit is contained in:
damocles 2026-08-18 12:37:52 +02:00 committed by mara
commit 2c45a9960f
9 changed files with 261 additions and 13 deletions

View file

@ -34,6 +34,7 @@ pub mod pr_create;
pub mod pr_merge;
pub mod pr_reviews;
pub mod pr_status;
pub mod reaction;
pub mod reopen;
pub mod repo_add_collaborator;
pub mod repo_create;
@ -47,7 +48,7 @@ pub mod view;
use std::fmt::Write as _;
use anyhow::Result;
use forgejo_api::structs::Attachment;
use forgejo_api::structs::{Attachment, Reaction};
use serde_json::{Value, json};
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
@ -380,6 +381,79 @@ pub(crate) fn dependency_summaries(
.collect())
}
/// The current reactions on an issue or PR itself (not a comment) — each
/// entry's `content` is Forgejo's shortcode (`"+1"`, `"heart"`, …, the
/// same vocabulary GitHub uses), `user` its login. Same shared-index
/// rationale as [`dependency_summaries`]: PRs are issues internally, so
/// `issue`/`pr`/`view` and the `reaction` verb's own listing all call
/// this instead of duplicating the fetch-and-shape step.
///
/// # Errors
///
/// Propagates the forge API errors from listing reactions.
pub(crate) fn issue_reactions(
client: &Client,
owner: &str,
name: &str,
number: u64,
) -> Result<Vec<Value>> {
let (_, reactions) = client
.api()
.issue_get_issue_reactions(owner, name, index(number)?)
.send()?;
Ok(reaction_values(reactions))
}
/// The current reactions on a single comment, by comment id (not the
/// parent issue/PR number — Forgejo's comment-reaction endpoints are
/// keyed on the comment alone, same as [`comment_show`]'s lookup).
///
/// # Errors
///
/// Propagates the forge API errors from listing reactions.
pub(crate) fn comment_reactions(
client: &Client,
owner: &str,
name: &str,
comment_id: u64,
) -> Result<Vec<Value>> {
let reactions = client
.api()
.issue_get_comment_reactions(owner, name, index(comment_id)?)
.send()?;
Ok(reaction_values(reactions))
}
fn reaction_values(reactions: Vec<Reaction>) -> Vec<Value> {
reactions
.into_iter()
.map(|r| json!({ "content": r.content, "user": r.user.and_then(|u| u.login) }))
.collect()
}
/// A one-line `content×count` summary of a reaction list (e.g. `+1×2
/// heart×1`), grouped and sorted by content name for a stable rendering.
/// `None` for an empty list, so a row/section with no reactions omits the
/// summary entirely rather than printing something empty.
pub(crate) fn reaction_summary(reactions: &[Value]) -> Option<String> {
let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
for r in reactions {
if let Some(content) = r.get("content").and_then(Value::as_str) {
*counts.entry(content).or_insert(0) += 1;
}
}
if counts.is_empty() {
return None;
}
Some(
counts
.into_iter()
.map(|(content, n)| format!("{content}×{n}"))
.collect::<Vec<_>>()
.join(" "),
)
}
/// A single attachment as one display line — `[file: <name>] <url>`,
/// mirroring the `[file: ...]` marker convention `read_room` already
/// uses for matrix attachments. `None` when the attachment has no