use std::collections::HashMap;
use std::env;
use airtable_api::{Airtable, Record, User as AirtableUser};
use chrono::naive::NaiveDate;
use chrono::offset::Utc;
use chrono::DateTime;
use chrono_humanize::HumanTime;
use serde::{Deserialize, Serialize};
use serde_json::Value;
static BASE_ID_CUSTOMER_LEADS: &str = "appr7imQLcR3pWaNa";
static MAILING_LIST_SIGNUPS_TABLE: &str = "Mailing List Signups";
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JournalClubMeeting {
pub title: String,
pub issue: String,
pub papers: Vec<Paper>,
pub date: NaiveDate,
pub coordinator: String,
pub state: String,
pub recording: String,
}
impl JournalClubMeeting {
pub fn as_slack_msg(&self) -> Value {
let mut color = "#ED64A6";
if self.state == "closed" {
color = "#ED8936";
}
let mut objects: Vec<Value> = Default::default();
if !self.recording.is_empty() {
objects.push(json!({
"elements": [{
"text": format!("<{}|Meeting recording>", self.recording),
"type": "mrkdwn"
}],
"type": "context"
}));
}
for p in self.papers.clone() {
let mut title = p.title.to_string();
if p.title == self.title {
title = "Paper".to_string();
}
objects.push(json!({
"elements": [{
"text": format!("<{}|{}>", p.link, title),
"type": "mrkdwn"
}],
"type": "context"
}));
}
json!({
"response_type": "in_channel",
"attachments": [{
"blocks": [{
"text": {
"text": format!("<{}|*{}*>", self.issue, self.title),
"type": "mrkdwn"
},
"type": "section"
},
{
"elements": [{
"text": "<https://github.com/oxidecomputer/papers/blob/master/os/countering-ipc-threats-minix3.pdf|Countering IPC Threats in Multiserver Operating Systems>",
"type": "mrkdwn"
}],
"type": "context"
},
{
"elements": [{
"text": format!("<https://github.com/{}|@{}> | {} | status: *{}*",self.coordinator,self.coordinator,self.date.format("%m/%d/%Y"),self.state),
"type": "mrkdwn"
}],
"type": "context"
}],
"color": color
}]
})
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Paper {
pub title: String,
pub link: String,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct RFD {
pub number: String,
pub title: String,
pub link: String,
pub state: String,
pub discussion: String,
}
impl RFD {
pub fn as_slack_msg(&self, num: i32) -> String {
let mut msg = format!("RFD {} {} (_*{}*_) <https://{}.rfd.oxide.computer|github> <https://rfd.shared.oxide.computer/rfd/{}|rendered>", num, self.title, self.state, num, self.number);
if !self.discussion.is_empty() {
msg += &format!(" <{}|discussion>", self.discussion);
}
msg
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RFDFields {
#[serde(rename = "Number")]
pub number: i32,
#[serde(rename = "State")]
pub state: String,
#[serde(rename = "Title")]
pub title: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "Name")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "Link")]
pub link: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CustomerInteractionFields {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Company")]
pub company: Vec<String>,
#[serde(with = "meeting_date_format", rename = "Date")]
pub date: NaiveDate,
#[serde(rename = "Type")]
pub meeting_type: String,
#[serde(rename = "Phase")]
pub phase: String,
#[serde(rename = "People")]
pub people: Vec<String>,
#[serde(rename = "Oxide Folks")]
pub oxide_folks: Vec<AirtableUser>,
#[serde(skip_serializing_if = "Option::is_none", rename = "Link to Notes")]
pub notes_link: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "Notes")]
pub notes: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DiscussionFields {
#[serde(rename = "Topic")]
pub topic: String,
#[serde(rename = "Submitter")]
pub submitter: AirtableUser,
#[serde(rename = "Priority")]
pub priority: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "Notes")]
pub notes: Option<String>,
#[serde(rename = "Associated meetings")]
pub associated_meetings: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MailingListSignupFields {
#[serde(rename = "Email Address")]
pub email: String,
#[serde(rename = "First Name")]
pub first_name: String,
#[serde(rename = "Last Name")]
pub last_name: String,
#[serde(rename = "Company")]
pub company: String,
#[serde(rename = "What is your interest in Oxide Computer Company?")]
pub interest: String,
#[serde(rename = "Interested in On the Metal podcast updates?")]
pub wants_podcast_updates: bool,
#[serde(rename = "Interested in the Oxide newsletter?")]
pub wants_newsletter: bool,
#[serde(rename = "Interested in product updates?")]
pub wants_product_updates: bool,
#[serde(rename = "Date Added")]
pub date_added: DateTime<Utc>,
#[serde(rename = "Opt-in Date")]
pub optin_date: DateTime<Utc>,
#[serde(rename = "Last Changed")]
pub last_changed: DateTime<Utc>,
}
impl MailingListSignupFields {
pub fn new(params: HashMap<String, String>) -> Self {
let email = if let Some(e) = params.get("data[email]") {
e.trim().to_string()
} else {
"".to_string()
};
let first_name = if let Some(f) = params.get("data[merges][FNAME]") {
f.trim().to_string()
} else {
"".to_string()
};
let last_name = if let Some(l) = params.get("data[merges][LNAME]") {
l.trim().to_string()
} else {
"".to_string()
};
let company = if let Some(c) = params.get("data[merges][COMPANY]") {
c.trim().to_string()
} else {
"".to_string()
};
let interest = if let Some(i) = params.get("data[merges][INTEREST]") {
i.trim().to_string()
} else {
"".to_string()
};
let wants_podcast_updates =
params.get("data[merges][GROUPINGS][0][groups]").is_some();
let wants_newsletter =
params.get("data[merges][GROUPINGS][1][groups]").is_some();
let wants_product_updates =
params.get("data[merges][GROUPINGS][2][groups]").is_some();
let time: DateTime<Utc> = if let Some(f) = params.get("fired_at") {
DateTime::parse_from_str(
&(f.to_owned() + " +00:00"),
"%Y-%m-%d %H:%M:%S %:z",
)
.unwrap()
.with_timezone(&Utc)
} else {
println!(
"could not parse mailchimp date time so defaulting to now"
);
Utc::now()
};
MailingListSignupFields {
email,
first_name,
last_name,
company,
interest,
wants_podcast_updates,
wants_newsletter,
wants_product_updates,
date_added: time,
optin_date: time,
last_changed: time,
}
}
pub async fn push_to_airtable(&self) {
let api_key = env::var("AIRTABLE_API_KEY").unwrap();
let airtable =
Airtable::new(api_key.to_string(), BASE_ID_CUSTOMER_LEADS);
let record = Record {
id: None,
created_time: None,
fields: serde_json::to_value(self).unwrap(),
};
airtable
.create_records(MAILING_LIST_SIGNUPS_TABLE, vec![record])
.await
.unwrap();
println!("created mailing list record in airtable: {:?}", self);
}
pub fn as_slack_msg(&self) -> Value {
let dur = self.date_added - Utc::now();
let time = HumanTime::from(dur);
let mut msg = format!(
"*{} {}* <mailto:{}|{}>",
self.first_name, self.last_name, self.email, self.email
);
if !self.interest.is_empty() {
msg += &format!("\n>{}", self.interest.trim());
}
let updates = format!(
"podcast updates: _{}_ | newsletter: _{}_ | product updates: _{}_",
self.wants_podcast_updates,
self.wants_newsletter,
self.wants_product_updates
);
let mut context = "".to_string();
if !self.company.is_empty() {
context += &format!("works at {} | ", self.company);
}
context += &format!("subscribed to mailing list {}", time);
json!({
"attachments": [
{
"color": "#F6E05E",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": msg
}
},
{
"type": "context",
"elements": [{
"type": "mrkdwn",
"text": updates
}]
},
{
"type": "context",
"elements": [{
"type": "mrkdwn",
"text": context
}]
}
]
}
]
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MeetingFields {
#[serde(rename = "Name")]
pub name: String,
#[serde(with = "meeting_date_format", rename = "Date")]
pub date: NaiveDate,
#[serde(rename = "Week")]
pub week: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "Notes")]
pub notes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "Action items")]
pub action_items: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "Proposed discussion"
)]
pub proposed_discussion: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "Recording")]
pub recording: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "Attendees")]
pub attendees: Option<Vec<AirtableUser>>,
}
mod meeting_date_format {
use chrono::naive::NaiveDate;
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &str = "%Y-%m-%d";
pub fn serialize<S>(
date: &NaiveDate,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = format!("{}", date.format(FORMAT));
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(NaiveDate::parse_from_str(&s, FORMAT).unwrap())
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct ProductEmailData {
pub date: String,
pub topics: Vec<DiscussionFields>,
pub last_meeting_reports_link: String,
pub meeting_id: String,
pub should_send: bool,
}