Skip to content

Commit a8a9c0a

Browse files
authored
Merge branch 'main' into feat/openapi-migration
2 parents 7dfcb23 + 7c827cf commit a8a9c0a

4 files changed

Lines changed: 112 additions & 18 deletions

File tree

src/config.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ mod tests {
6060

6161
impl EnvGuard {
6262
fn new() -> Self {
63-
// NOTE: Keep in sync with provider env vars that affect test behavior
64-
const KEYS: [&str; 27] = [
63+
const KEYS: &[&str] = &[
6564
"SPACEBOT_DIR",
6665
"SPACEBOT_DEPLOYMENT",
6766
"SPACEBOT_CRON_TIMEZONE",
6867
"SPACEBOT_USER_TIMEZONE",
6968
"ANTHROPIC_API_KEY",
7069
"ANTHROPIC_BASE_URL",
70+
"ANTHROPIC_AUTH_TOKEN",
7171
"ANTHROPIC_OAUTH_TOKEN",
7272
"OPENAI_API_KEY",
7373
"OPENROUTER_API_KEY",
@@ -89,14 +89,15 @@ mod tests {
8989
"MINIMAX_CN_API_KEY",
9090
"MOONSHOT_API_KEY",
9191
"ZAI_CODING_PLAN_API_KEY",
92+
"GITHUB_COPILOT_API_KEY",
9293
];
9394

9495
let vars = KEYS
95-
.into_iter()
96-
.map(|key| (key, std::env::var(key).ok()))
96+
.iter()
97+
.map(|&key| (key, std::env::var(key).ok()))
9798
.collect::<Vec<_>>();
9899

99-
for key in KEYS {
100+
for &key in KEYS {
100101
unsafe {
101102
std::env::remove_var(key);
102103
}

src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,17 @@ impl OutboundResponse {
748748
{
749749
lines.push(footer.text.trim().to_string());
750750
}
751+
if let Some(author) = &card.author
752+
&& !author.name.trim().is_empty()
753+
{
754+
lines.push(author.name.trim().to_string());
755+
}
756+
if let Some(timestamp) = &card.timestamp
757+
&& !timestamp.trim().is_empty()
758+
&& chrono::DateTime::parse_from_rfc3339(timestamp.trim()).is_ok()
759+
{
760+
lines.push(timestamp.trim().to_string());
761+
}
751762
if !lines.is_empty() {
752763
sections.push(lines.join("\n\n"));
753764
}
@@ -767,6 +778,14 @@ pub struct Card {
767778
pub fields: Vec<CardField>,
768779
#[serde(default, deserialize_with = "deserialize_card_footer")]
769780
pub footer: Option<CardFooter>,
781+
/// Small image in the top-right corner of the embed.
782+
pub thumbnail: Option<CardImage>,
783+
/// Large image at the bottom of the embed.
784+
pub image: Option<CardImage>,
785+
/// Author bar at the top of the embed.
786+
pub author: Option<CardAuthor>,
787+
/// ISO 8601 timestamp displayed in the footer area.
788+
pub timestamp: Option<String>,
770789
}
771790

772791
/// A card footer that can be either a plain string or a structured object.
@@ -874,6 +893,20 @@ pub struct CardField {
874893
pub inline: bool,
875894
}
876895

896+
/// Image (thumbnail or main image) for a Card.
897+
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
898+
pub struct CardImage {
899+
pub url: String,
900+
}
901+
902+
/// Author for a Card.
903+
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
904+
pub struct CardAuthor {
905+
pub name: String,
906+
pub url: Option<String>,
907+
pub icon_url: Option<String>,
908+
}
909+
877910
/// Container for interactive elements (maps to ActionRows in Discord).
878911
/// In Discord, an action row can contain either buttons or a single select menu.
879912
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]

src/messaging/discord.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ use arc_swap::ArcSwap;
1010
use async_trait::async_trait;
1111
use serenity::all::{
1212
ButtonStyle, ChannelId, ChannelType, Context, CreateActionRow, CreateAttachment, CreateButton,
13-
CreateEmbed, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage,
14-
CreateMessage, CreatePoll, CreatePollAnswer, CreateSelectMenu, CreateSelectMenuKind,
15-
CreateSelectMenuOption, CreateThread, EditMessage, EventHandler, GatewayIntents, GetMessages,
16-
Http, Interaction, Message, MessageId, ReactionType, Ready, ShardManager, User, UserId,
13+
CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateInteractionResponse,
14+
CreateInteractionResponseMessage, CreateMessage, CreatePoll, CreatePollAnswer,
15+
CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption, CreateThread, EditMessage,
16+
EventHandler, GatewayIntents, GetMessages, Http, Interaction, Message, MessageId, ReactionType,
17+
Ready, ShardManager, Timestamp, User, UserId,
1718
};
1819
use std::collections::HashMap;
1920
use std::sync::Arc;
@@ -1038,11 +1039,39 @@ fn build_embed(card: &crate::Card) -> CreateEmbed {
10381039
embed = embed.url(url);
10391040
}
10401041
if let Some(footer) = &card.footer {
1041-
let mut discord_footer = CreateEmbedFooter::new(footer.text.clone());
1042-
if let Some(icon_url) = &footer.icon_url {
1043-
discord_footer = discord_footer.icon_url(icon_url);
1042+
let footer_text = footer.text.trim();
1043+
if !footer_text.is_empty() {
1044+
let mut footer_builder = CreateEmbedFooter::new(footer_text);
1045+
if let Some(icon_url) = &footer.icon_url {
1046+
footer_builder = footer_builder.icon_url(icon_url);
1047+
}
1048+
embed = embed.footer(footer_builder);
1049+
}
1050+
}
1051+
if let Some(thumbnail) = &card.thumbnail {
1052+
embed = embed.thumbnail(&thumbnail.url);
1053+
}
1054+
if let Some(image) = &card.image {
1055+
embed = embed.image(&image.url);
1056+
}
1057+
if let Some(author) = &card.author {
1058+
let author_name = author.name.trim();
1059+
if !author_name.is_empty() {
1060+
let mut author_builder = CreateEmbedAuthor::new(author_name);
1061+
if let Some(url) = &author.url {
1062+
author_builder = author_builder.url(url);
1063+
}
1064+
if let Some(icon_url) = &author.icon_url {
1065+
author_builder = author_builder.icon_url(icon_url);
1066+
}
1067+
embed = embed.author(author_builder);
1068+
}
1069+
}
1070+
if let Some(timestamp) = &card.timestamp {
1071+
match timestamp.parse::<Timestamp>() {
1072+
Ok(ts) => embed = embed.timestamp(ts),
1073+
Err(e) => tracing::warn!(timestamp, %e, "invalid ISO 8601 timestamp in card, skipping"),
10441074
}
1045-
embed = embed.footer(discord_footer);
10461075
}
10471076

10481077
for (i, field) in card.fields.iter().enumerate() {
@@ -1319,10 +1348,7 @@ mod tests {
13191348
let cards = vec![Card {
13201349
title: Some("Status".into()),
13211350
description: Some("All green".into()),
1322-
color: None,
1323-
url: None,
1324-
fields: Vec::new(),
1325-
footer: None,
1351+
..Default::default()
13261352
}];
13271353

13281354
let parts = prepare_rich_message_parts(String::new(), &cards, &[], None);

src/tools/reply.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,41 @@ impl Tool for ReplyTool {
285285
"required": ["name", "value"]
286286
}
287287
},
288-
"footer": { "type": "string" }
288+
"footer": {
289+
"type": "object",
290+
"properties": {
291+
"text": { "type": "string" },
292+
"icon_url": { "type": "string", "format": "uri" }
293+
},
294+
"required": ["text"]
295+
},
296+
"thumbnail": {
297+
"type": "object",
298+
"description": "Small image in the top-right corner of the embed.",
299+
"properties": { "url": { "type": "string", "format": "uri" } },
300+
"required": ["url"]
301+
},
302+
"image": {
303+
"type": "object",
304+
"description": "Large image at the bottom of the embed.",
305+
"properties": { "url": { "type": "string", "format": "uri" } },
306+
"required": ["url"]
307+
},
308+
"author": {
309+
"type": "object",
310+
"description": "Author bar at the top of the embed.",
311+
"properties": {
312+
"name": { "type": "string" },
313+
"url": { "type": "string", "format": "uri" },
314+
"icon_url": { "type": "string", "format": "uri" }
315+
},
316+
"required": ["name"]
317+
},
318+
"timestamp": {
319+
"type": "string",
320+
"format": "date-time",
321+
"description": "ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z) displayed in the footer area."
322+
}
289323
}
290324
}
291325
},

0 commit comments

Comments
 (0)