Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ mod tests {

impl EnvGuard {
fn new() -> Self {
// NOTE: Keep in sync with provider env vars that affect test behavior
const KEYS: [&str; 27] = [
const KEYS: &[&str] = &[
"SPACEBOT_DIR",
"SPACEBOT_DEPLOYMENT",
"SPACEBOT_CRON_TIMEZONE",
"SPACEBOT_USER_TIMEZONE",
"ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_OAUTH_TOKEN",
"OPENAI_API_KEY",
"OPENROUTER_API_KEY",
Expand All @@ -89,14 +89,15 @@ mod tests {
"MINIMAX_CN_API_KEY",
"MOONSHOT_API_KEY",
"ZAI_CODING_PLAN_API_KEY",
"GITHUB_COPILOT_API_KEY",
];

let vars = KEYS
.into_iter()
.map(|key| (key, std::env::var(key).ok()))
.iter()
.map(|&key| (key, std::env::var(key).ok()))
.collect::<Vec<_>>();

for key in KEYS {
for &key in KEYS {
unsafe {
std::env::remove_var(key);
}
Expand Down
33 changes: 33 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,17 @@ impl OutboundResponse {
{
lines.push(footer.text.trim().to_string());
}
if let Some(author) = &card.author
&& !author.name.trim().is_empty()
{
lines.push(author.name.trim().to_string());
}
if let Some(timestamp) = &card.timestamp
&& !timestamp.trim().is_empty()
&& chrono::DateTime::parse_from_rfc3339(timestamp.trim()).is_ok()
{
lines.push(timestamp.trim().to_string());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if !lines.is_empty() {
sections.push(lines.join("\n\n"));
}
Expand All @@ -759,6 +770,14 @@ pub struct Card {
pub fields: Vec<CardField>,
#[serde(default, deserialize_with = "deserialize_card_footer")]
pub footer: Option<CardFooter>,
/// Small image in the top-right corner of the embed.
pub thumbnail: Option<CardImage>,
/// Large image at the bottom of the embed.
pub image: Option<CardImage>,
/// Author bar at the top of the embed.
pub author: Option<CardAuthor>,
/// ISO 8601 timestamp displayed in the footer area.
pub timestamp: Option<String>,
}

/// A card footer that can be either a plain string or a structured object.
Expand Down Expand Up @@ -866,6 +885,20 @@ pub struct CardField {
pub inline: bool,
}

/// Image (thumbnail or main image) for a Card.
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CardImage {
pub url: String,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Author for a Card.
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CardAuthor {
pub name: String,
pub url: Option<String>,
pub icon_url: Option<String>,
}

/// Container for interactive elements (maps to ActionRows in Discord).
/// In Discord, an action row can contain either buttons or a single select menu.
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
Expand Down
50 changes: 38 additions & 12 deletions src/messaging/discord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ use arc_swap::ArcSwap;
use async_trait::async_trait;
use serenity::all::{
ButtonStyle, ChannelId, ChannelType, Context, CreateActionRow, CreateAttachment, CreateButton,
CreateEmbed, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage,
CreateMessage, CreatePoll, CreatePollAnswer, CreateSelectMenu, CreateSelectMenuKind,
CreateSelectMenuOption, CreateThread, EditMessage, EventHandler, GatewayIntents, GetMessages,
Http, Interaction, Message, MessageId, ReactionType, Ready, ShardManager, User, UserId,
CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateMessage, CreatePoll, CreatePollAnswer,
CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption, CreateThread, EditMessage,
EventHandler, GatewayIntents, GetMessages, Http, Interaction, Message, MessageId, ReactionType,
Ready, ShardManager, Timestamp, User, UserId,
};
use std::collections::HashMap;
use std::sync::Arc;
Expand Down Expand Up @@ -1038,11 +1039,39 @@ fn build_embed(card: &crate::Card) -> CreateEmbed {
embed = embed.url(url);
}
if let Some(footer) = &card.footer {
Comment thread
vsumner marked this conversation as resolved.
let mut discord_footer = CreateEmbedFooter::new(footer.text.clone());
if let Some(icon_url) = &footer.icon_url {
discord_footer = discord_footer.icon_url(icon_url);
let footer_text = footer.text.trim();
if !footer_text.is_empty() {
let mut footer_builder = CreateEmbedFooter::new(footer_text);
if let Some(icon_url) = &footer.icon_url {
footer_builder = footer_builder.icon_url(icon_url);
}
embed = embed.footer(footer_builder);
}
}
if let Some(thumbnail) = &card.thumbnail {
embed = embed.thumbnail(&thumbnail.url);
}
if let Some(image) = &card.image {
embed = embed.image(&image.url);
}
if let Some(author) = &card.author {
let author_name = author.name.trim();
if !author_name.is_empty() {
let mut author_builder = CreateEmbedAuthor::new(author_name);
if let Some(url) = &author.url {
author_builder = author_builder.url(url);
}
if let Some(icon_url) = &author.icon_url {
author_builder = author_builder.icon_url(icon_url);
}
embed = embed.author(author_builder);
}
}
if let Some(timestamp) = &card.timestamp {
match timestamp.parse::<Timestamp>() {
Ok(ts) => embed = embed.timestamp(ts),
Err(e) => tracing::warn!(timestamp, %e, "invalid ISO 8601 timestamp in card, skipping"),
}
embed = embed.footer(discord_footer);
}

for (i, field) in card.fields.iter().enumerate() {
Expand Down Expand Up @@ -1319,10 +1348,7 @@ mod tests {
let cards = vec![Card {
title: Some("Status".into()),
description: Some("All green".into()),
color: None,
url: None,
fields: Vec::new(),
footer: None,
..Default::default()
}];

let parts = prepare_rich_message_parts(String::new(), &cards, &[], None);
Expand Down
36 changes: 35 additions & 1 deletion src/tools/reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,41 @@ impl Tool for ReplyTool {
"required": ["name", "value"]
}
},
"footer": { "type": "string" }
"footer": {
"type": "object",
"properties": {
"text": { "type": "string" },
"icon_url": { "type": "string", "format": "uri" }
},
"required": ["text"]
},
"thumbnail": {
"type": "object",
"description": "Small image in the top-right corner of the embed.",
"properties": { "url": { "type": "string", "format": "uri" } },
"required": ["url"]
},
"image": {
"type": "object",
"description": "Large image at the bottom of the embed.",
"properties": { "url": { "type": "string", "format": "uri" } },
"required": ["url"]
},
"author": {
"type": "object",
"description": "Author bar at the top of the embed.",
"properties": {
"name": { "type": "string" },
"url": { "type": "string", "format": "uri" },
"icon_url": { "type": "string", "format": "uri" }
},
"required": ["name"]
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z) displayed in the footer area."
}
}
}
},
Expand Down
Loading