Skip to content
Open
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
Binary file modified .env.ci
Binary file not shown.
8 changes: 4 additions & 4 deletions .github/scripts/build-image/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ async function main() {
const gitSha = (await $`git rev-parse --short HEAD`.text()).trim();

const tags = [
`tahminator/codebloom:${tagPrefix}latest`,
`tahminator/codebloom:${tagPrefix}${timestamp}`,
`tahminator/codebloom:${tagPrefix}${gitSha}`,
`patinanetwork/codebloom:${tagPrefix}latest`,
`patinanetwork/codebloom:${tagPrefix}${timestamp}`,
`patinanetwork/codebloom:${tagPrefix}${gitSha}`,
];

console.log("Building image with following tags:");
Expand All @@ -85,7 +85,7 @@ async function main() {
console.log("DOCKER_HUB_PAT missing or empty");
}

await $`echo ${dockerHubPat} | docker login -u tahminator --password-stdin`;
await $`echo ${dockerHubPat} | docker login -u patinanetwork --password-stdin`;

try {
await $`docker buildx create --use --name codebloom-builder`;
Expand Down
8 changes: 4 additions & 4 deletions .github/scripts/build-image/internal/standup-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ async function main() {
const gitSha = (await $`git rev-parse --short HEAD`.text()).trim();

const tags = [
`tahminator/codebloom-standup-bot:latest`,
`tahminator/codebloom-standup-bot:${timestamp}`,
`tahminator/codebloom-standup-bot:${gitSha}`,
`patinanetwork/codebloom-standup-bot:latest`,
`patinanetwork/codebloom-standup-bot:${timestamp}`,
`patinanetwork/codebloom-standup-bot:${gitSha}`,
];

console.log("Building image with following tags:");
Expand All @@ -61,7 +61,7 @@ async function main() {
console.log("DOCKER_HUB_PAT missing or empty");
}

await $`echo ${dockerHubPat} | docker login -u tahminator --password-stdin`;
await $`echo ${dockerHubPat} | docker login -u patinanetwork --password-stdin`;

try {
await $`docker buildx create --use --name codebloom-standup-bot-builder`;
Expand Down
57 changes: 53 additions & 4 deletions internal/standup-bot/src/discord/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ use serenity::{
Client,
Error,
all::{
Cache,
ChannelId,
Colour,
CreateEmbed,
CreateEmbedFooter,
CreateMessage,
CreateThread,
GatewayIntents,
GuildId,
Member,
RoleId,
UserId,
},
http::Http,
};
Expand All @@ -23,16 +28,18 @@ use crate::{
credentials::DiscordCredentials,
handlers::Handler,
},
redis,
utils::latch::base::{
CountdownLatch,
Latch,
},
};

const INTENTS: GatewayIntents = GatewayIntents::GUILD_MEMBERS;
const INTENTS: GatewayIntents = GatewayIntents::GUILD_MEMBERS.union(GatewayIntents::GUILD_MESSAGES);
const ROLE_ID: u64 = 1391944565409316944;

static HTTP: OnceLock<Arc<Http>> = OnceLock::new();
static CACHE: OnceLock<Arc<Cache>> = OnceLock::new();

pub async fn init_in_bg(discord_creds: &DiscordCredentials) -> Result<(), Error> {
let latch = CountdownLatch::new(1);
Expand All @@ -47,6 +54,11 @@ pub async fn init_in_bg(discord_creds: &DiscordCredentials) -> Result<(), Error>
.set(http)
.inspect_err(|_| println!("Attempted to save Discord HTTP client more than once"));

let cache = client.cache.clone();
let _ = CACHE
.set(cache)
.inspect_err(|_| println!("Attempted to save Discord Cache more than once"));

tokio::spawn(async move {
if let Err(e) = client.start().await {
println!("Client error: {e:?}");
Expand All @@ -62,6 +74,10 @@ pub fn get_http() -> Arc<Http> {
HTTP.get().expect("Discord not initialized").clone()
}

pub fn get_cache() -> Arc<Cache> {
CACHE.get().expect("Discord not initialized").clone()
}

pub async fn send_standup_message(discord_creds: &DiscordCredentials) -> Result<(), Error> {
println!("Sending standup message!");

Expand All @@ -87,9 +103,42 @@ pub async fn send_standup_message(discord_creds: &DiscordCredentials) -> Result<

let thread_builder = CreateThread::new("Daily Standup Thread");

channel
let thread = channel
.create_thread_from_message(get_http().as_ref(), msg.id, thread_builder)
.await
.inspect_err(|e| println!("Error creating thread: {e:#?}"))
.map(|_| ())
.inspect_err(|e| println!("Error creating thread: {e:#?}"))?;

if let Err(e) = redis::client::set_standup_thread_id(thread.id.get()).await {
eprintln!("Failed to persist standup thread id: {e:#?}");
}

Ok(())
}

pub async fn get_standup_role_members(guild_id: u64) -> Result<Vec<Member>, Error> {
let guild = GuildId::new(guild_id);
let role = RoleId::new(ROLE_ID);

let members = get_cache()
.guild(guild)
.map(|g| {
g.members
.values()
.filter(|m| m.roles.contains(&role))
.cloned()
.collect()
})
.unwrap_or_default();

Ok(members)
}

pub async fn send_eod_reminder_dm(user_id: u64) -> Result<(), Error> {
let user = UserId::new(user_id).to_user(get_http().as_ref()).await?;

let message = CreateMessage::new().content(
"Reminder: you haven't posted your update in today's standup thread yet. Please add one before end of day!",
);

user.dm(get_http().as_ref(), message).await.map(|_| ())
}
42 changes: 42 additions & 0 deletions internal/standup-bot/src/discord/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use chrono::Utc;
use chrono_tz::US;
use serenity::{
all::{
ChunkGuildFilter,
Command,
Context,
CreateInteractionResponse,
CreateInteractionResponseMessage,
EventHandler,
GuildId,
Interaction,
Message,
Ready,
},
async_trait,
Expand All @@ -17,6 +21,7 @@ use crate::{
commands,
credentials,
},
redis,
utils::latch::base::{
CountdownLatch,
Latch as _,
Expand Down Expand Up @@ -51,6 +56,31 @@ impl EventHandler for Handler {
}
}

async fn message(&self, _ctx: Context, new_message: Message) {
if new_message.author.bot {
return;
}

let thread_id = match redis::client::get_standup_thread_id().await {
Ok(Some(id)) => id,
Ok(None) => return,
Err(e) => {
eprintln!("Failed to get standup thread id: {e:#?}");
return;
}
};

if new_message.channel_id.get() != thread_id {
return;
}

let today = Utc::now().with_timezone(&US::Eastern).date_naive();
if let Err(e) = redis::client::add_standup_reply(today, new_message.author.id.get()).await
{
eprintln!("Failed to record standup reply: {e:#?}");
}
}

async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);

Expand All @@ -61,6 +91,18 @@ impl EventHandler for Handler {

self.latch.count_down();
}

async fn cache_ready(&self, ctx: Context, _guilds: Vec<GuildId>) {
let creds = credentials::get_discord_credentials();

ctx.shard.chunk_guild(
GuildId::new(creds.guild_id),
None,
false,
ChunkGuildFilter::None,
None,
);
}
}

pub async fn get_commands(guild_id: u64, ctx: Context) -> Result<Vec<Command>, serenity::Error> {
Expand Down
56 changes: 55 additions & 1 deletion internal/standup-bot/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::error::Error;

use chrono::Utc;
use chrono_tz::US;
use dotenvy::dotenv;
use tokio::time::interval;

use crate::utils::standup::is_time_to_send_standup_message;
use crate::utils::standup::{
is_time_to_send_eod_reminder,
is_time_to_send_standup_message,
};

mod discord;
mod redis;
Expand Down Expand Up @@ -52,5 +56,55 @@ async fn main() -> Result<(), Box<dyn Error>> {
}
}
});

let cloned_discord_creds = discord_creds.clone();
tokio::spawn(async move {
let today = Utc::now().with_timezone(&US::Eastern).date_naive();

let already_sent = match redis::client::get_eod_reminder_sent(today).await {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to get eod_reminder_sent from Redis: {e:#?}");
return;
}
};

if !is_time_to_send_eod_reminder(already_sent) {
return;
}

let members =
match discord::client::get_standup_role_members(cloned_discord_creds.guild_id)
.await
{
Ok(m) => m,
Err(e) => {
eprintln!("Failed to fetch standup role members: {e:#?}");
return;
}
};

let replied = match redis::client::get_standup_replies(today).await {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to get standup replies from Redis: {e:#?}");
return;
}
};

for member in members {
let user_id = member.user.id.get();
if replied.contains(&user_id) {
continue;
}
if let Err(e) = discord::client::send_eod_reminder_dm(user_id).await {
eprintln!("Failed to send EOD reminder DM to {user_id}: {e:#?}");
}
}

if let Err(e) = redis::client::set_eod_reminder_sent(today).await {
eprintln!("Failed to save eod_reminder_sent to Redis: {e:#?}");
}
});
}
}
53 changes: 53 additions & 0 deletions internal/standup-bot/src/redis/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ use bb8_redis::{
};
use chrono::{
DateTime,
NaiveDate,
Utc,
};
use std::collections::HashSet;
use std::sync::OnceLock;

use crate::redis::{
Expand Down Expand Up @@ -58,3 +60,54 @@ pub async fn get_last_standup() -> Result<Option<DateTime<Utc>>, RedisClientErro
.transpose()
.map_err(RedisClientError::from)
}

pub async fn set_standup_thread_id(thread_id: u64) -> Result<(), RedisClientError> {
let mut conn = get_pool().await?;

Ok(conn
.set("standup:current_thread_id", thread_id.to_string())
.await?)
}

pub async fn get_standup_thread_id() -> Result<Option<u64>, RedisClientError> {
let mut conn = get_pool().await?;

let value: Option<String> = conn.get("standup:current_thread_id").await?;

Ok(value.and_then(|v| v.parse::<u64>().ok()))
}

pub async fn add_standup_reply(date: NaiveDate, user_id: u64) -> Result<(), RedisClientError> {
let mut conn = get_pool().await?;

Ok(conn
.sadd(format!("standup:{date}:replied"), user_id.to_string())
.await?)
}

pub async fn get_standup_replies(date: NaiveDate) -> Result<HashSet<u64>, RedisClientError> {
let mut conn = get_pool().await?;

let members: Vec<String> = conn.smembers(format!("standup:{date}:replied")).await?;

Ok(members
.into_iter()
.filter_map(|v| v.parse::<u64>().ok())
.collect())
}

pub async fn set_eod_reminder_sent(date: NaiveDate) -> Result<(), RedisClientError> {
let mut conn = get_pool().await?;

Ok(conn
.set(format!("standup:{date}:eod_reminder_sent"), "1")
.await?)
}

pub async fn get_eod_reminder_sent(date: NaiveDate) -> Result<bool, RedisClientError> {
let mut conn = get_pool().await?;

let value: Option<String> = conn.get(format!("standup:{date}:eod_reminder_sent")).await?;

Ok(value.is_some())
}
12 changes: 12 additions & 0 deletions internal/standup-bot/src/utils/standup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ pub fn is_time_to_send_standup_message(last_standup_time: Option<DateTime<Utc>>)
None => true,
}
}

pub fn is_time_to_send_eod_reminder(already_sent_today: bool) -> bool {
let now = Utc::now().with_timezone(&US::Eastern);
let is_reminder_day = now.weekday() == Weekday::Sat;
let is_reminder_time = now.hour() >= 17;

if !(is_reminder_day && is_reminder_time) {
return false;
}

!already_sent_today
}
5 changes: 0 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,6 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.6.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
Expand Down
Loading
Loading