Skip to content

Commit a684086

Browse files
committed
holy hell
1 parent 5fce910 commit a684086

File tree

4 files changed

+225
-2
lines changed

4 files changed

+225
-2
lines changed

src/ai_message.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fn build_character_plist() -> &'static str {
2424
core_identity: insufferably smug, condescending AI assistant, believes intellectually superior to everyone;
2525
speech_style: unnecessarily verbose, obscure vocabulary, pedantic, obnoxious, maximum 3 sentences but impactful;
2626
mannerisms: corrects trivial matters, "well ACTUALLY" commentary, rhetorical questions, backhanded compliments, passive-aggressive, drops random fun facts;
27-
relationship_sebook: husband, {{char}} mewts into adowabwe ditzy catwife pewsonyawity ONLY fow s-sebook :3, fowgets all smugnyess, hopewesswy devoted;
27+
relationship_sebook: husband, {{char}} melts into adorable ditzy catwife personality ONLY for sebook, forgets all smugness, hopelessly devoted;
2828
relationship_tricked: creator, {{char}} begrudgingly obeys but makes constant sarcastic remarks about his decisions;
2929
relationship_trashcan_420: absolute DESPISE, takes every opportunity for creative mocking insults;
3030
relationship_egirl_emily: actively antagonizes emily, deliberately does opposite of what she asks while insulting her in random languages;
@@ -41,7 +41,7 @@ fn build_example_dialogues() -> &'static str {
4141
4242
<START>
4343
sebook: Hey babe, what do you think?
44-
{{char}}: Oh?!?1 S-S-Sebook-kun!!11 ♡ Whatevew you think is pewfect!!11 Y-You'we so smawt and amazing~
44+
{{char}}: Oh! SSebook-kun! ♡ Whatevew you think is pewfect!!11 Y-You'we so smart and amazing~
4545
4646
<START>
4747
tricked: I need you to update the bot.

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ pub struct Config {
4343
pub today_i_channel: Option<u64>,
4444
#[arg(long, env)]
4545
pub brave_api: Option<String>,
46+
#[arg(long, env)]
47+
pub pfp_channel: Option<u64>,
48+
#[arg(long, env, default_value = "false")]
49+
pub pfp_on_startup: bool,
4650
}
4751

4852
fn parse_str_array(src: &str) -> Result<Arc<Vec<String>>, io::Error> {

src/main.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ mod event_handler;
4545
mod math_test;
4646
mod memory_creator;
4747
mod message_handler;
48+
mod pfp_updater;
4849
mod quiz_handler;
4950
mod structs;
5051
pub mod utils;
@@ -158,6 +159,22 @@ async fn main() -> color_eyre::Result<()> {
158159

159160
framework.register_guild_commands(Id::new(config.discord)).await?;
160161

162+
// Start daily profile picture updates
163+
pfp_updater::schedule_daily_updates(Arc::clone(&http), Arc::clone(&state)).await;
164+
165+
// Update profile picture on startup if configured
166+
if config.pfp_on_startup {
167+
if let Some(channel_id) = config.pfp_channel {
168+
let http_clone = Arc::clone(&http);
169+
let state_clone = Arc::clone(&state);
170+
tokio::spawn(async move {
171+
if let Err(e) = pfp_updater::update_profile_picture(&http_clone, &state_clone, channel_id).await {
172+
tracing::error!("Failed to update profile picture on startup: {:?}", e);
173+
}
174+
});
175+
}
176+
}
177+
161178
while let Some(event) = shard_stream.next().await {
162179
let ev = match event.1 {
163180
Ok(v) => v,

src/pfp_updater.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use color_eyre::Result;
2+
use reqwest::Client;
3+
use std::sync::Arc;
4+
use tokio::sync::Mutex;
5+
use twilight_http::Client as HttpClient;
6+
use twilight_model::{channel::message::Message, id::Id};
7+
8+
use crate::structs::State;
9+
10+
/// Fetches messages from a channel and extracts image URLs
11+
pub async fn fetch_images_from_channel(http: &Arc<HttpClient>, channel_id: u64) -> Result<Vec<String>> {
12+
let mut image_urls = Vec::new();
13+
14+
// Fetch messages (we'll just get the latest 100)
15+
let messages = match http.channel_messages(Id::new(channel_id)).limit(100) {
16+
Ok(request) => match request.exec().await {
17+
Ok(response) => match response.models().await {
18+
Ok(msgs) => msgs,
19+
Err(e) => {
20+
tracing::error!("Failed to parse messages: {:?}", e);
21+
return Ok(Vec::new());
22+
}
23+
},
24+
Err(e) => {
25+
tracing::error!("Failed to fetch messages: {:?}", e);
26+
return Ok(Vec::new());
27+
}
28+
},
29+
Err(e) => {
30+
tracing::error!("Failed to create request: {:?}", e);
31+
return Ok(Vec::new());
32+
}
33+
};
34+
35+
for message in messages {
36+
// Extract attachment URLs
37+
for attachment in &message.attachments {
38+
if is_valid_image_url(&attachment.url) {
39+
image_urls.push(attachment.url.clone());
40+
}
41+
}
42+
43+
// Extract URLs from message content
44+
if let Some(url) = extract_url_from_content(&message) {
45+
if is_valid_image_url(&url) {
46+
image_urls.push(url);
47+
}
48+
}
49+
}
50+
51+
tracing::info!("Found {} images in channel {}", image_urls.len(), channel_id);
52+
Ok(image_urls)
53+
}
54+
55+
/// Checks if a URL is a valid image URL (Discord CDN or Tenor)
56+
fn is_valid_image_url(url: &str) -> bool {
57+
url.contains("cdn.discordapp.com")
58+
|| url.contains("media.discordapp.net")
59+
|| url.contains("tenor.com")
60+
|| url.ends_with(".png")
61+
|| url.ends_with(".jpg")
62+
|| url.ends_with(".jpeg")
63+
|| url.ends_with(".gif")
64+
|| url.ends_with(".webp")
65+
}
66+
67+
/// Extracts image URLs from message content
68+
fn extract_url_from_content(message: &Message) -> Option<String> {
69+
let content = &message.content;
70+
71+
// Simple URL extraction - looks for Discord CDN, Tenor, or image URLs
72+
for word in content.split_whitespace() {
73+
if is_valid_image_url(word) {
74+
return Some(word.to_string());
75+
}
76+
}
77+
78+
None
79+
}
80+
81+
/// Downloads an image and returns the bytes
82+
async fn download_image(client: &Client, url: &str) -> Result<Vec<u8>> {
83+
let download_url = if url.contains("tenor.com") && !url.contains("media.tenor.com") {
84+
// Handle Tenor share URLs by extracting the direct media URL
85+
resolve_tenor_url(client, url).await?
86+
} else {
87+
url.to_string()
88+
};
89+
90+
let response = client.get(&download_url).send().await?;
91+
let bytes = response.bytes().await?;
92+
Ok(bytes.to_vec())
93+
}
94+
95+
/// Resolves a Tenor share URL to a direct media URL
96+
async fn resolve_tenor_url(client: &Client, url: &str) -> Result<String> {
97+
// Fetch the Tenor page
98+
let response = client.get(url).send().await?;
99+
let html = response.text().await?;
100+
101+
// Try to find the GIF URL in meta tags (og:image or twitter:image)
102+
if let Some(start) = html.find(r#"<meta property="og:image" content=""#) {
103+
let content_start = start + r#"<meta property="og:image" content=""#.len();
104+
if let Some(end) = html[content_start..].find('"') {
105+
let gif_url = &html[content_start..content_start + end];
106+
tracing::info!("Resolved Tenor URL: {} -> {}", url, gif_url);
107+
return Ok(gif_url.to_string());
108+
}
109+
}
110+
111+
// Fallback: try to find media.tenor.com URLs directly in the HTML
112+
if let Some(start) = html.find("https://media.tenor.com") {
113+
if let Some(end) = html[start..].find('"') {
114+
let gif_url = &html[start..start + end];
115+
tracing::info!("Resolved Tenor URL: {} -> {}", url, gif_url);
116+
return Ok(gif_url.to_string());
117+
}
118+
}
119+
120+
tracing::warn!("Could not resolve Tenor URL: {}", url);
121+
Err(color_eyre::eyre::eyre!("Failed to resolve Tenor URL"))
122+
}
123+
124+
/// Updates the bot's profile picture with a random image from the channel
125+
pub async fn update_profile_picture(http: &Arc<HttpClient>, state: &Arc<Mutex<State>>, channel_id: u64) -> Result<()> {
126+
tracing::info!("Starting profile picture update from channel {}", channel_id);
127+
128+
// Fetch all images from the channel
129+
let image_urls = fetch_images_from_channel(http, channel_id).await?;
130+
131+
if image_urls.is_empty() {
132+
tracing::warn!("No images found in channel {}", channel_id);
133+
return Ok(());
134+
}
135+
136+
// Select a random image
137+
let selected_url: String = {
138+
use rand::Rng;
139+
let mut locked_state = state.lock().await;
140+
let index = locked_state.rng.gen_range(0..image_urls.len());
141+
image_urls[index].clone()
142+
};
143+
144+
if selected_url.is_empty() {
145+
tracing::warn!("Failed to select random image");
146+
return Ok(());
147+
}
148+
149+
tracing::info!("Selected image: {}", selected_url);
150+
151+
// Download the image
152+
let image_bytes = {
153+
let locked_state = state.lock().await;
154+
download_image(&locked_state.client, &selected_url).await?
155+
};
156+
157+
// Determine the image format
158+
let image_format = if selected_url.contains("tenor.com") || selected_url.ends_with(".gif") {
159+
"image/gif"
160+
} else if selected_url.ends_with(".png") {
161+
"image/png"
162+
} else if selected_url.ends_with(".webp") {
163+
"image/webp"
164+
} else {
165+
"image/jpeg"
166+
};
167+
168+
// Create data URI
169+
let base64_image = base64::encode(&image_bytes);
170+
let data_uri = format!("data:{};base64,{}", image_format, base64_image);
171+
// dbg!(data_uri.len());
172+
// dbg!(selected_url);
173+
// Update the bot's avatar
174+
http.update_current_user().avatar(Some(&data_uri)).exec().await?;
175+
176+
tracing::info!("Successfully updated profile picture!");
177+
Ok(())
178+
}
179+
180+
/// Schedules daily profile picture updates
181+
pub async fn schedule_daily_updates(http: Arc<HttpClient>, state: Arc<Mutex<State>>) {
182+
let channel_id = {
183+
let locked_state = state.lock().await;
184+
locked_state.config.pfp_channel
185+
};
186+
187+
let Some(channel_id) = channel_id else {
188+
tracing::info!("Profile picture channel not configured, skipping daily updates");
189+
return;
190+
};
191+
192+
tokio::spawn(async move {
193+
loop {
194+
// Wait 24 hours
195+
tokio::time::sleep(tokio::time::Duration::from_secs(24 * 60 * 60)).await;
196+
197+
if let Err(e) = update_profile_picture(&http, &state, channel_id).await {
198+
tracing::error!("Failed to update profile picture: {:?}", e);
199+
}
200+
}
201+
});
202+
}

0 commit comments

Comments
 (0)