use std::borrow::Cow;
use std::fmt::Debug;
use aoc_leaderboard::aoc::LeaderboardCredentials;
use aoc_leaderbot_aws_lib::leaderbot::storage::aws::dynamodb::DynamoDbStorage;
use aoc_leaderbot_lib::leaderbot::config::env::get_env_config;
use aoc_leaderbot_lib::leaderbot::config::mem::MemoryConfig;
use aoc_leaderbot_lib::leaderbot::{BotOutput, Config, Reporter, run_bot_from};
use aoc_leaderbot_slack_lib::leaderbot::reporter::slack::webhook::{
LeaderboardSortOrder, SlackWebhookReporter,
};
use lambda_runtime::{Error, LambdaEvent};
use serde::{Deserialize, Serialize};
use tracing::{debug, info, trace};
use veil::Redact;
#[derive(Debug, Default, Clone, Deserialize)]
pub struct IncomingMessage {
#[serde(default)]
pub year: Option<i32>,
#[serde(default)]
pub leaderboard_id: Option<u64>,
#[serde(default)]
pub credentials: Option<LeaderboardCredentials>,
#[serde(default)]
pub test_run: bool,
#[cfg(feature = "__testing")]
#[doc(hidden)]
#[serde(default)]
pub aoc_base_url: Option<String>,
#[serde(flatten)]
pub dynamodb_storage_input: IncomingDynamoDbStorageInput,
#[serde(flatten)]
pub slack_webhook_reporter_input: IncomingSlackWebhookReporterInput,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct IncomingDynamoDbStorageInput {
pub table_name: Option<String>,
#[cfg(feature = "__testing")]
#[doc(hidden)]
pub test_endpoint_url: Option<String>,
#[cfg(feature = "__testing")]
#[doc(hidden)]
pub test_region: Option<String>,
}
#[derive(Redact, Default, Clone, Deserialize)]
#[serde(default)]
pub struct IncomingSlackWebhookReporterInput {
#[redact(partial)]
pub webhook_url: Option<String>,
pub channel: Option<String>,
pub username: Option<String>,
pub icon_url: Option<String>,
pub sort_order: Option<LeaderboardSortOrder>,
}
#[derive(Debug, Clone, Serialize)]
pub struct OutgoingMessage {
pub output: BotOutput,
}
pub const CONFIG_ENV_VAR_PREFIX: &str = "AOC_LEADERBOT_AWS_";
pub const DEFAULT_DYNAMODB_TABLE_NAME: &str = "aoc_leaderbot";
#[cfg_attr(not(coverage), tracing::instrument(ret, err))]
pub async fn bot_lambda_handler(
event: LambdaEvent<IncomingMessage>,
) -> Result<OutgoingMessage, Error> {
let input = event.payload;
let config = get_config(&input)?;
let mut storage = get_storage(&input).await;
let mut reporter = get_reporter(&input)?;
#[cfg(feature = "__testing")]
let advent_of_code_base = input.aoc_base_url;
#[cfg(not(feature = "__testing"))]
let advent_of_code_base: Option<String> = None;
trace!("Running bot (test run: {})", input.test_run);
let output =
run_bot_from(advent_of_code_base, &config, &mut storage, &mut reporter, input.test_run)
.await?;
if input.test_run {
let previous_leaderboard = output
.previous_leaderboard
.as_ref()
.unwrap_or(&output.leaderboard);
let changes = output
.changes
.as_ref()
.map(Cow::Borrowed)
.unwrap_or_default();
info!("Test run: reporting changes");
debug!(?previous_leaderboard, ?changes);
reporter
.report_changes(
output.year,
output.leaderboard_id,
config.credentials().view_key(),
previous_leaderboard,
&output.leaderboard,
&changes,
)
.await?;
}
Ok(OutgoingMessage { output })
}
#[cfg_attr(not(coverage), tracing::instrument(err))]
fn get_config(input: &IncomingMessage) -> Result<MemoryConfig, Error> {
let (year, leaderboard_id, credentials) =
match (input.year, input.leaderboard_id, input.credentials.clone()) {
(Some(year), Some(leaderboard_id), Some(credentials)) => {
(year, leaderboard_id, credentials)
},
(year, leaderboard_id, credentials) => {
let env_config = get_env_config(CONFIG_ENV_VAR_PREFIX)?;
(
year.unwrap_or_else(|| env_config.year()),
leaderboard_id.unwrap_or_else(|| env_config.leaderboard_id()),
credentials.unwrap_or_else(|| env_config.credentials()),
)
},
};
debug!(year, leaderboard_id, ?credentials);
Ok(MemoryConfig::builder()
.year(year)
.leaderboard_id(leaderboard_id)
.credentials(credentials)
.build()
.expect("all fields should have been specified"))
}
#[cfg_attr(not(coverage), tracing::instrument)]
async fn get_storage(input: &IncomingMessage) -> DynamoDbStorage {
#[cfg(feature = "__testing")]
#[cfg_attr(coverage_nightly, coverage(off))]
async fn internal_get_storage(input: &IncomingMessage, table_name: String) -> DynamoDbStorage {
match input.dynamodb_storage_input.test_endpoint_url.as_ref() {
Some(endpoint_url) => {
let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(aws_config::Region::new(
input
.dynamodb_storage_input
.test_region
.as_ref()
.map(|region| Cow::Owned(region.clone()))
.unwrap_or_else(|| Cow::Borrowed("ca-central-1")),
))
.endpoint_url(endpoint_url)
.test_credentials()
.load()
.await;
DynamoDbStorage::with_config(&config, table_name).await
},
None => DynamoDbStorage::new(table_name).await,
}
}
#[cfg(not(feature = "__testing"))]
async fn internal_get_storage(_input: &IncomingMessage, table_name: String) -> DynamoDbStorage {
DynamoDbStorage::new(table_name).await
}
let table_name = input
.dynamodb_storage_input
.table_name
.clone()
.unwrap_or_else(|| DEFAULT_DYNAMODB_TABLE_NAME.into());
internal_get_storage(input, table_name).await
}
#[cfg_attr(not(coverage), tracing::instrument(err))]
fn get_reporter(input: &IncomingMessage) -> Result<SlackWebhookReporter, Error> {
let mut builder = SlackWebhookReporter::builder();
if let Some(webhook_url) = input.slack_webhook_reporter_input.webhook_url.clone() {
builder.webhook_url(webhook_url);
}
if let Some(channel) = input.slack_webhook_reporter_input.channel.clone() {
builder.channel(channel);
}
if let Some(username) = input.slack_webhook_reporter_input.username.clone() {
builder.username(username);
}
if let Some(icon_url) = input.slack_webhook_reporter_input.icon_url.clone() {
builder.icon_url(icon_url);
}
if let Some(sort_order) = input.slack_webhook_reporter_input.sort_order {
builder.sort_order(sort_order);
}
Ok(builder.build()?)
}