From f83cbd8ab0bf77b41b9f5d5dba6d517449ca1fbc Mon Sep 17 00:00:00 2001 From: LTurret Date: Wed, 14 Jan 2026 17:33:00 +0800 Subject: [PATCH 01/22] =?UTF-8?q?=E2=9C=A8=20Dynamically=20fetch=20content?= =?UTF-8?q?=20by=20struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + src/commands/embed.rs | 80 ++++++++------------------------------- src/commands/instagram.rs | 29 ++++++++++++++ src/commands/twitter.rs | 49 +++++++++++++++++++++++- 4 files changed, 93 insertions(+), 66 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 69201a2..5b5c617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ authors = ["LTurret "] edition = "2024" [dependencies] +async-trait = "0.1.89" dotenv = "0.15.0" regex = "1.12.2" reqwest = "0.12.24" diff --git a/src/commands/embed.rs b/src/commands/embed.rs index f722cd9..f13eaff 100644 --- a/src/commands/embed.rs +++ b/src/commands/embed.rs @@ -1,86 +1,36 @@ -use crate::commands::twitter::Tweet; -use regex::{Captures, Regex}; -use reqwest::{header::USER_AGENT, Client as HttpClient, Error}; +// use crate::commands::{instagram::InstagramFetcher, twitter::TweetFetcher}; +use crate::commands::twitter::TweetFetcher; +use async_trait::async_trait; +use regex::Captures; use serenity::{builder::CreateMessage, prelude::*}; pub struct Embed; +#[async_trait] +pub trait ContentFetcher: Send + Sync { + async fn fetch_json(&self, endpoint: &str, ctx: &Context) -> CreateMessage; +} + impl Embed { pub async fn new_embed(&self, ctx: &Context, caps: &Captures<'_>) -> CreateMessage { - let endpoint: String = caps + let endpoint: &str = caps .name("endpoint") .expect("Expected a valid haystack") - .as_str() - .to_string(); + .as_str(); // Regex Pattern: (http|https)://(?.+)\.com(?(/.+)*) - let raw_api_data: String = match caps + let fetcher: Box = match caps .name("domain") .expect("Expacted a valid haystack") .as_str() { - "x" | "twitter" => self - .fetch_tweet_json(endpoint) - .await - .expect("Err while fetching FxTwitter API"), - "instagram" => self - .fetch_instagram_json(endpoint) - .await - .expect("Err while fetching Instagram post"), + "x" | "twitter" => Box::new(TweetFetcher), + // "instagram" => Box::new(InstagramFetcher), _ => unimplemented!(), }; - let tweet: Tweet = Tweet::from_raw(&ctx, raw_api_data).await; - let embed_message: CreateMessage = tweet.to_embed().await; + let embed_message: CreateMessage = fetcher.fetch_json(&endpoint, ctx).await; embed_message } - - async fn fetch_tweet_json(&self, endpoint: String) -> Result { - let caps: Captures<'_> = Regex::new(r"(?/.+/status/[0-9]+)(\?.=.+)*") - .expect("Expected a valid regex pattern") - .captures(endpoint.as_str()) - .expect("Expected a valid haystack"); - - let api_url: String = format!( - "https://api.fxtwitter.com{}", - caps.name("tweet_endpoint") - .expect("Expected a valid haystack") - .as_str() - ); - - let client: HttpClient = HttpClient::new(); - let api_json: String = client - .get(api_url) - .header( - USER_AGENT, - "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)", - ) - .send() - .await? - .text() - .await?; - - Ok(api_json) - } - - async fn fetch_instagram_json(&self, endpoint: String) -> Result { - let clean_endpoint = format!( - "https://www.instagram.com/{}/", - Regex::new(r"(?(/p/.+)/(.+))") - .expect("Expected a valid regex pattern") - .captures(endpoint.as_str()) - .expect("Expected a valid haystack") - .get(0) - .expect("Expected a valid matching") - .as_str() - .to_string() - ); - - let client: HttpClient = HttpClient::new(); - let api_json: String = client.get(clean_endpoint).send().await?.text().await?; - println!("{:#?}", &api_json); - - Ok(api_json) - } } diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index dd1f26d..256a7ca 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -1,6 +1,35 @@ +// use crate::commands::embed::ContentFetcher; +// use async_trait::async_trait; +// use regex::{Captures, Regex}; use regex::Captures; +// use reqwest::{Client as HttpClient, Error}; use serenity::{builder::CreateMessage, prelude::*}; +// pub struct InstagramFetcher; + +// #[async_trait] +// impl ContentFetcher for InstagramFetcher { +// async fn fetch_json(&self, endpoint: &str, ctx: &Context) -> Result { +// let clean_endpoint = format!( +// "https://www.instagram.com/{}/", +// Regex::new(r"(?(/p/.+)/(.+))") +// .expect("Expected a valid regex pattern") +// .captures(endpoint) +// .expect("Expected a valid haystack") +// .get(0) +// .expect("Expected a valid matching") +// .as_str() +// .to_string() +// ); + +// let client: HttpClient = HttpClient::new(); +// let api_json: String = client.get(clean_endpoint).send().await?.text().await?; +// println!("{:#?}", &api_json); + +// Ok(api_json) +// } +// } + pub async fn handler(_ctx: &Context, _caps: &Captures<'_>) -> CreateMessage { let message = CreateMessage::new(); message diff --git a/src/commands/twitter.rs b/src/commands/twitter.rs index 41b1ff0..46e60ec 100644 --- a/src/commands/twitter.rs +++ b/src/commands/twitter.rs @@ -1,5 +1,10 @@ -use crate::commands::{author::Author, embed::Embed}; +use crate::commands::{ + author::Author, + embed::{ContentFetcher, Embed}, +}; +use async_trait::async_trait; use regex::{Captures, Regex}; +use reqwest::{header::USER_AGENT, Client as HttpClient}; use serde_json::{from_str, Value}; use serenity::{ builder::{ @@ -134,6 +139,48 @@ impl Tweet { } } +pub struct TweetFetcher; + +#[async_trait] +impl ContentFetcher for TweetFetcher { + async fn fetch_json(&self, endpoint: &str, ctx: &Context) -> CreateMessage { + let caps: Captures<'_> = Regex::new(r"(?/.+/status/[0-9]+)(\?.=.+)*") + .expect("Expected a valid regex pattern") + .captures(endpoint) + .expect("Expected a valid haystack"); + + let api_url: String = format!( + "https://api.fxtwitter.com{}", + caps.name("tweet_endpoint") + .expect("Expected a valid haystack") + .as_str() + ); + + let client: HttpClient = HttpClient::new(); + let response_result = client + .get(api_url) + .header( + USER_AGENT, + "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)", + ) + .send() + .await; + + let response = match response_result { + Ok(resp) => resp, + Err(err) => { + eprintln!("{}", err); + return CreateMessage::new().content("Failed to fetch tweet"); + } + }; + + let api_json = response.text().await.expect("Failed to read response text"); + let tweet: Tweet = Tweet::from_raw(&ctx, api_json).await; + let embed_message: CreateMessage = tweet.to_embed().await; + embed_message + } +} + pub async fn handler(ctx: &Context, caps: &Captures<'_>) -> CreateMessage { let embed_message = Embed.new_embed(ctx, caps).await; embed_message From 2a6572c7973fcd8f3ace89cd4cfa6d9bb3ad1ae6 Mon Sep 17 00:00:00 2001 From: LTurret Date: Thu, 15 Jan 2026 14:30:48 +0800 Subject: [PATCH 02/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20variable=20?= =?UTF-8?q?to=20better=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/twitter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/twitter.rs b/src/commands/twitter.rs index 46e60ec..1b49348 100644 --- a/src/commands/twitter.rs +++ b/src/commands/twitter.rs @@ -29,7 +29,7 @@ pub struct Tweet { } impl Tweet { - pub async fn from_raw(ctx: &Context, raw_api_data: String) -> Self { + pub async fn from_raw_api(ctx: &Context, raw_api_data: String) -> Self { let json_api_data: Value = from_str(raw_api_data.as_str()).expect("Expected a valid payload"); @@ -175,7 +175,7 @@ impl ContentFetcher for TweetFetcher { }; let api_json = response.text().await.expect("Failed to read response text"); - let tweet: Tweet = Tweet::from_raw(&ctx, api_json).await; + let tweet: Tweet = Tweet::from_raw_api(&ctx, api_json).await; let embed_message: CreateMessage = tweet.to_embed().await; embed_message } From 63d1a36ca22831f28e0db7c3190f3f2446ab48c9 Mon Sep 17 00:00:00 2001 From: LTurret Date: Fri, 16 Jan 2026 12:49:50 +0800 Subject: [PATCH 03/22] =?UTF-8?q?=F0=9F=92=A1=20Add=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/patcher.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/patcher.rs b/src/commands/patcher.rs index 1cc2000..ea9193f 100644 --- a/src/commands/patcher.rs +++ b/src/commands/patcher.rs @@ -25,6 +25,7 @@ impl Patcher { .captures(&self.msg.content) .expect("Expected a valid haystack"); + // Decide domain let embed_message: CreateMessage = match caps .name("domain") .expect("Expected domain in haystack") @@ -39,6 +40,7 @@ impl Patcher { _ => unimplemented!(), }; + // Sending embed message if let Err(why) = self .msg .channel_id From 3f0b6c9f2d8c27fafa74218e6bf663f3cc6bd0dc Mon Sep 17 00:00:00 2001 From: LTurret Date: Fri, 16 Jan 2026 12:51:01 +0800 Subject: [PATCH 04/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20From=20public=20func?= =?UTF-8?q?tion=20to=20private?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/twitter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/twitter.rs b/src/commands/twitter.rs index 1b49348..56f120c 100644 --- a/src/commands/twitter.rs +++ b/src/commands/twitter.rs @@ -29,7 +29,7 @@ pub struct Tweet { } impl Tweet { - pub async fn from_raw_api(ctx: &Context, raw_api_data: String) -> Self { + async fn from_raw_api(ctx: &Context, raw_api_data: String) -> Self { let json_api_data: Value = from_str(raw_api_data.as_str()).expect("Expected a valid payload"); @@ -105,7 +105,7 @@ impl Tweet { } } - pub async fn to_embed(self) -> CreateMessage { + async fn to_embed(self) -> CreateMessage { let embed: CreateEmbed = CreateEmbed::new() .color(Color::new(0x00b0f4)) .author( From f65eabd886edcc0ea5fb8eeec60a9ebac753e82d Mon Sep 17 00:00:00 2001 From: LTurret Date: Fri, 16 Jan 2026 14:23:45 +0800 Subject: [PATCH 05/22] =?UTF-8?q?=F0=9F=9A=A7=20Instagram=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + src/commands/embed.rs | 5 +- src/commands/instagram.rs | 165 ++++++++++++++++++++++++++++++-------- 3 files changed, 133 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5b5c617..e3a4881 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" [dependencies] async-trait = "0.1.89" dotenv = "0.15.0" +html-escape = "0.2.13" regex = "1.12.2" reqwest = "0.12.24" serde_json = "1.0.145" diff --git a/src/commands/embed.rs b/src/commands/embed.rs index f13eaff..7f35e41 100644 --- a/src/commands/embed.rs +++ b/src/commands/embed.rs @@ -1,5 +1,4 @@ -// use crate::commands::{instagram::InstagramFetcher, twitter::TweetFetcher}; -use crate::commands::twitter::TweetFetcher; +use crate::commands::{instagram::InstagramFetcher, twitter::TweetFetcher}; use async_trait::async_trait; use regex::Captures; use serenity::{builder::CreateMessage, prelude::*}; @@ -25,7 +24,7 @@ impl Embed { .as_str() { "x" | "twitter" => Box::new(TweetFetcher), - // "instagram" => Box::new(InstagramFetcher), + "instagram" => Box::new(InstagramFetcher), _ => unimplemented!(), }; diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 256a7ca..0cc5d5a 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -1,36 +1,131 @@ -// use crate::commands::embed::ContentFetcher; -// use async_trait::async_trait; -// use regex::{Captures, Regex}; -use regex::Captures; -// use reqwest::{Client as HttpClient, Error}; -use serenity::{builder::CreateMessage, prelude::*}; - -// pub struct InstagramFetcher; - -// #[async_trait] -// impl ContentFetcher for InstagramFetcher { -// async fn fetch_json(&self, endpoint: &str, ctx: &Context) -> Result { -// let clean_endpoint = format!( -// "https://www.instagram.com/{}/", -// Regex::new(r"(?(/p/.+)/(.+))") -// .expect("Expected a valid regex pattern") -// .captures(endpoint) -// .expect("Expected a valid haystack") -// .get(0) -// .expect("Expected a valid matching") -// .as_str() -// .to_string() -// ); - -// let client: HttpClient = HttpClient::new(); -// let api_json: String = client.get(clean_endpoint).send().await?.text().await?; -// println!("{:#?}", &api_json); - -// Ok(api_json) -// } -// } - -pub async fn handler(_ctx: &Context, _caps: &Captures<'_>) -> CreateMessage { - let message = CreateMessage::new(); - message +use crate::commands::{ + author::Author, + embed::{ContentFetcher, Embed}, +}; +use async_trait::async_trait; +use html_escape::decode_html_entities; +use regex::{Captures, Regex}; +use reqwest::{header::USER_AGENT, Client as HttpClient}; +use serde_json::{json, Value}; +use serenity::{ + builder::{ + CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, + }, + model::Color, + prelude::*, +}; + +#[derive(Debug)] +pub struct InstagramPost { + pub author: Author, + pub content: String, + pub videos_supplementary: String, +} + +impl InstagramPost { + async fn from_raw_response(raw_response: String) -> Self { + let author = Regex::new( + r#".+)/p/"#, + ) + .unwrap() + .captures(&raw_response) + .unwrap() + .name("author") + .unwrap() + .as_str() + .to_string(); + + let content = Regex::new( + r#"(?s).+)".+> = decode_html_entities(&content).chars().collect(); + let content: String = content_chars[2..content_chars.len() - 1].iter().collect(); + + let author_json: Value = json!({ + "url": "https://www.instagram.com/p/DS71PSjiQIu/", + "name": author, + "screen_name": author, + "icon_url": "https://static.cdninstagram.com/rsrc.php/v4/yI/r/VsNE-OHk_8a.png", + }); + + let videos_supplementary: String = String::from(""); + + Self { + author: Author::from_json(&author_json), + content: content, + videos_supplementary: videos_supplementary, + } + } + + async fn to_embed(self) -> CreateMessage { + let embed: CreateEmbed = CreateEmbed::new() + .color(Color::new(0xce0071)) + .author(CreateEmbedAuthor::new(format!("{}", self.author.name))) + .description(self.content) + .footer( + CreateEmbedFooter::new("Instagram") + .icon_url("https://images-ext-1.discordapp.net/external/C6jCIKlXguRhfmSp6USkbWsS11fnsbBgMXiclR2R4ps/https/www.instagram.com/static/images/ico/favicon-192.png/68d99ba29cc8.png"), + ) + .url("https://lturret.xyz"); + + let builder: CreateMessage = CreateMessage::new() + .content(&self.videos_supplementary) + .allowed_mentions(CreateAllowedMentions::new().empty_users()) + .embed(embed); + + builder + } +} + +pub struct InstagramFetcher; + +#[async_trait] +impl ContentFetcher for InstagramFetcher { + async fn fetch_json(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { + let clean_endpoint = format!( + "https://www.instagram.com/p/{}/", + Regex::new(r"/p/(?.+)/") + .expect("Expected a valid regex pattern") + .captures(endpoint) + .expect("Expected a valid haystack") + .name("post_id") + .expect("Expected a valid matching") + .as_str() + .to_string() + ); + + let client: HttpClient = HttpClient::new(); + let response_result = client + .get(clean_endpoint) + .header( + USER_AGENT, + "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)", + ) + .send() + .await; + + let response: String = match response_result { + Ok(resp) => resp.text().await.expect("Failed to read response text"), + Err(err) => { + eprintln!("{}", err); + return CreateMessage::new().content("Failed to fetch post"); + } + }; + + let instagram_post: InstagramPost = InstagramPost::from_raw_response(response).await; + let embed_message: CreateMessage = instagram_post.to_embed().await; + embed_message + } +} + +pub async fn handler(ctx: &Context, caps: &Captures<'_>) -> CreateMessage { + let embed_message = Embed.new_embed(ctx, caps).await; + embed_message } From 6676bf5e1889e0fb40d457a1927b30aa9b947804 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 18 Jan 2026 15:58:56 +0800 Subject: [PATCH 06/22] =?UTF-8?q?=F0=9F=92=A1=20Add=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/embed.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/embed.rs b/src/commands/embed.rs index 7f35e41..3965731 100644 --- a/src/commands/embed.rs +++ b/src/commands/embed.rs @@ -23,6 +23,7 @@ impl Embed { .expect("Expacted a valid haystack") .as_str() { + // ContentFetcher selector by domain "x" | "twitter" => Box::new(TweetFetcher), "instagram" => Box::new(InstagramFetcher), _ => unimplemented!(), From 754f7230bf2511dd2d17959f2e85edca62dd28e1 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 18 Jan 2026 16:11:18 +0800 Subject: [PATCH 07/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Add=20string=20parse?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/author.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/commands/author.rs b/src/commands/author.rs index 8b41404..4a013b5 100644 --- a/src/commands/author.rs +++ b/src/commands/author.rs @@ -21,4 +21,13 @@ impl Author { icon_url: get_string(json_data, "avatar_url"), } } + + pub fn from_str(url: &String, name: &String, screen_name: &String, icon_url: &String) -> Self { + Self { + url: String::from(url), + name: String::from(name), + screen_name: String::from(screen_name), + icon_url: String::from(icon_url), + } + } } From 9f14437e65394263547537dea70e45e95dd79206 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 18 Jan 2026 16:41:55 +0800 Subject: [PATCH 08/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Signature=20changed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/embed.rs | 4 ++-- src/commands/instagram.rs | 2 +- src/commands/twitter.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/embed.rs b/src/commands/embed.rs index 3965731..51cd268 100644 --- a/src/commands/embed.rs +++ b/src/commands/embed.rs @@ -7,7 +7,7 @@ pub struct Embed; #[async_trait] pub trait ContentFetcher: Send + Sync { - async fn fetch_json(&self, endpoint: &str, ctx: &Context) -> CreateMessage; + async fn embed_message(&self, endpoint: &str, ctx: &Context) -> CreateMessage; } impl Embed { @@ -29,7 +29,7 @@ impl Embed { _ => unimplemented!(), }; - let embed_message: CreateMessage = fetcher.fetch_json(&endpoint, ctx).await; + let embed_message: CreateMessage = fetcher.embed_message(&endpoint, ctx).await; embed_message } diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 0cc5d5a..5bf0a3f 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -88,7 +88,7 @@ pub struct InstagramFetcher; #[async_trait] impl ContentFetcher for InstagramFetcher { - async fn fetch_json(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { + async fn embed_message(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { let clean_endpoint = format!( "https://www.instagram.com/p/{}/", Regex::new(r"/p/(?.+)/") diff --git a/src/commands/twitter.rs b/src/commands/twitter.rs index 56f120c..fa85d34 100644 --- a/src/commands/twitter.rs +++ b/src/commands/twitter.rs @@ -143,7 +143,7 @@ pub struct TweetFetcher; #[async_trait] impl ContentFetcher for TweetFetcher { - async fn fetch_json(&self, endpoint: &str, ctx: &Context) -> CreateMessage { + async fn embed_message(&self, endpoint: &str, ctx: &Context) -> CreateMessage { let caps: Captures<'_> = Regex::new(r"(?/.+/status/[0-9]+)(\?.=.+)*") .expect("Expected a valid regex pattern") .captures(endpoint) From 42b7fea9d616eecea56b2dc515cbaafc8a708c0e Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 18 Jan 2026 16:42:47 +0800 Subject: [PATCH 09/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Implemented=20for=20?= =?UTF-8?q?string=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/instagram.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 5bf0a3f..5ff6df9 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -6,7 +6,6 @@ use async_trait::async_trait; use html_escape::decode_html_entities; use regex::{Captures, Regex}; use reqwest::{header::USER_AGENT, Client as HttpClient}; -use serde_json::{json, Value}; use serenity::{ builder::{ CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, @@ -24,7 +23,7 @@ pub struct InstagramPost { impl InstagramPost { async fn from_raw_response(raw_response: String) -> Self { - let author = Regex::new( + let author: String = Regex::new( r#".+)/p/"#, ) .unwrap() @@ -35,7 +34,7 @@ impl InstagramPost { .as_str() .to_string(); - let content = Regex::new( + let raw_content: &str = Regex::new( r#"(?s).+)".+> = decode_html_entities(&content).chars().collect(); + let content_chars: Vec = decode_html_entities(&raw_content).chars().collect(); let content: String = content_chars[2..content_chars.len() - 1].iter().collect(); - - let author_json: Value = json!({ - "url": "https://www.instagram.com/p/DS71PSjiQIu/", - "name": author, - "screen_name": author, - "icon_url": "https://static.cdninstagram.com/rsrc.php/v4/yI/r/VsNE-OHk_8a.png", - }); - let videos_supplementary: String = String::from(""); Self { - author: Author::from_json(&author_json), + author: Author::from_str(&String::from(""), &author, &author, &String::from("")), content: content, videos_supplementary: videos_supplementary, } From a8a68ec8ff88058f9b2d638f9f5a2d736f900bab Mon Sep 17 00:00:00 2001 From: LTurret Date: Mon, 19 Jan 2026 16:22:43 +0800 Subject: [PATCH 10/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Match=20naming=20sty?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/instagram.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 5ff6df9..8cffc70 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -103,9 +103,9 @@ impl ContentFetcher for InstagramFetcher { .await; let response: String = match response_result { - Ok(resp) => resp.text().await.expect("Failed to read response text"), - Err(err) => { - eprintln!("{}", err); + Ok(res) => res.text().await.expect("Failed to read response text"), + Err(e) => { + eprintln!("{}", e); return CreateMessage::new().content("Failed to fetch post"); } }; From 00068fd3069da6eb31bebc6ef41bf00c9696ccad Mon Sep 17 00:00:00 2001 From: LTurret Date: Mon, 19 Jan 2026 16:23:02 +0800 Subject: [PATCH 11/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Shorten=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/instagram.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 8cffc70..4537afc 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -92,8 +92,7 @@ impl ContentFetcher for InstagramFetcher { .to_string() ); - let client: HttpClient = HttpClient::new(); - let response_result = client + let response_result: Result = HttpClient::new() .get(clean_endpoint) .header( USER_AGENT, From 8e05c97ae4fddf94caa45d7ced1fc68c335fbdce Mon Sep 17 00:00:00 2001 From: LTurret Date: Mon, 19 Jan 2026 16:24:39 +0800 Subject: [PATCH 12/22] =?UTF-8?q?=F0=9F=9A=A7=20Instagram=20post=20parser?= =?UTF-8?q?=20(Plaintext)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Cargo.toml | 2 +- src/commands/instagram.rs | 113 +++++++++++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index fe06271..f0cb534 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env +.zed Cargo.lock target diff --git a/Cargo.toml b/Cargo.toml index e3a4881..ab7f4e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ async-trait = "0.1.89" dotenv = "0.15.0" html-escape = "0.2.13" regex = "1.12.2" -reqwest = "0.12.24" +reqwest = { version = "0.12.24", features = ["cookies", "json"] } serde_json = "1.0.145" serenity = { version = "0.12.4", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] } diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 4537afc..3828a7e 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -5,7 +5,11 @@ use crate::commands::{ use async_trait::async_trait; use html_escape::decode_html_entities; use regex::{Captures, Regex}; -use reqwest::{header::USER_AGENT, Client as HttpClient}; +use reqwest::{ + header::{HeaderMap, HeaderValue, USER_AGENT}, + Client as HttpClient, +}; +use serde_json::Value; use serenity::{ builder::{ CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, @@ -34,6 +38,9 @@ impl InstagramPost { .as_str() .to_string(); + let url: &String = &String::from(format!("https://www.instagram.com/{}", &author)); + let icon_url: String = InstagramPost::get_profile_pic_url(&author).await; + let raw_content: &str = Regex::new( r#"(?s).+)".+> String { + let mut headers = HeaderMap::new(); + headers.insert("x-ig-app-id", HeaderValue::from_static("936619743392459")); + + let response = reqwest::Client::new() + .get(format!( + "https://www.instagram.com/api/v1/users/web_profile_info/?username={}", + username + )) + .headers(headers) + .send() + .await + .expect("Request user manifest failed"); + + let res: Value = response.json().await.expect("JSON parse failed"); + + res["data"]["user"]["profile_pic_url"] + .as_str() + .unwrap_or(r#"Did not found "profile_pic_url" in the JSON"#) + .to_string() + } + + #[deprecated = "get_profile_pic_url() is enough to get user icon url"] + #[cfg(false)] + async fn get_icon_url(author: &String) -> String { + use reqwest::{ + cookie::{CookieStore, Jar}, + header::HeaderName, + Url, + }; + use std::sync::Arc; + + let user_id: String = InstagramPost::get_user_id(author).await; + let jar = Arc::new(Jar::default()); + let _client: Response = HttpClient::builder() + .cookie_provider(jar.clone()) + .user_agent("Mozilla/5.0") + .build() + .unwrap() + .get("https://www.instagram.com/") + .send() + .await + .unwrap(); + + let cookie_val: HeaderValue = jar + .as_ref() + .cookies(&Url::parse(format!("https://www.instagram.com/{}", author).as_str()).unwrap()) + .ok_or("no cookies found") + .unwrap(); + + let cookie_header: String = cookie_val.to_str().unwrap().to_string(); + let csrftoken: String = cookie_header + .split(';') + .find(|c| c.trim().starts_with("csrftoken=")) + .map(|c| { + c.trim() + .strip_prefix("csrftoken=") + .unwrap_or("") + .to_string() + }) + .expect("csrftoken not found"); + + let body = format!("variables=%7B%22enable_integrity_filters%22%3Atrue%2C%22id%22%3A%22{}%22%2C%22render_surface%22%3A%22PROFILE%22%2C%22__relay_internal__pv__PolarisCannesGuardianExperienceEnabledrelayprovider%22%3Atrue%2C%22__relay_internal__pv__PolarisCASB976ProfileEnabledrelayprovider%22%3Afalse%2C%22__relay_internal__pv__PolarisRepostsConsumptionEnabledrelayprovider%22%3Afalse%7D&doc_id=25980296051578533", user_id); + + let mut headers = HeaderMap::new(); + let cookie = format!("ig_did=B9C9BB5D-2753-46D0-9784-3C94B0FAD0C9;csrftoken={};datr=rl40aad9n6XXVIcGcEsaMfZU;mid=aTRergALAAGAj_Wk-MQWM3oJJrI3;ps_l=1; ps_n=1; ig_nrcb=1; wd=958x944", csrftoken); + + headers.insert( + HeaderName::from_static("cookie"), + HeaderValue::try_from(cookie).expect("cookie invalid"), + ); + headers.insert( + HeaderName::from_static("content-type"), + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + headers.insert( + HeaderName::from_static("x-csrftoken"), + HeaderValue::try_from(csrftoken).expect("x-csrftoken"), + ); + + let response = HttpClient::new() + .post("https://www.instagram.com/graphql/query") + .headers(headers) + .body(body) + .send() + .await; + + match response { + Ok(res) => { + res.text().await.unwrap(); + } + Err(e) => { + eprintln!("Response parse error: {}", e); + } + } + + let response = String::from("gay"); + response + } + async fn to_embed(self) -> CreateMessage { let embed: CreateEmbed = CreateEmbed::new() .color(Color::new(0xce0071)) - .author(CreateEmbedAuthor::new(format!("{}", self.author.name))) + .author(CreateEmbedAuthor::new(format!("{}", self.author.name)).icon_url(self.author.icon_url).url(self.author.url)) .description(self.content) .footer( CreateEmbedFooter::new("Instagram") From e941a40107e71fe8f8361a5ac5d9e926e5ce6d9d Mon Sep 17 00:00:00 2001 From: LTurret Date: Sat, 4 Apr 2026 03:59:10 +0800 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=90=9B=20Fix=20haystack=20incomplet?= =?UTF-8?q?e=20(deliminator=20added)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/instagram.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 3828a7e..68efd60 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -28,7 +28,7 @@ pub struct InstagramPost { impl InstagramPost { async fn from_raw_response(raw_response: String) -> Self { let author: String = Regex::new( - r#".+)/p/"#, + r#".+)\/p\/"#, ) .unwrap() .captures(&raw_response) @@ -189,7 +189,7 @@ impl ContentFetcher for InstagramFetcher { async fn embed_message(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { let clean_endpoint = format!( "https://www.instagram.com/p/{}/", - Regex::new(r"/p/(?.+)/") + Regex::new(r"\/p\/(?.+)\/") .expect("Expected a valid regex pattern") .captures(endpoint) .expect("Expected a valid haystack") From a4c0a7ff790badd997db8c9e3d8bd615b979c200 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sat, 4 Apr 2026 04:00:07 +0800 Subject: [PATCH 14/22] =?UTF-8?q?=F0=9F=92=A5=20Meta=20changed=20their=20A?= =?UTF-8?q?PI=20so=20this=20won't=20work=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/patcher.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/patcher.rs b/src/commands/patcher.rs index ea9193f..030f2ee 100644 --- a/src/commands/patcher.rs +++ b/src/commands/patcher.rs @@ -32,7 +32,8 @@ impl Patcher { .as_str() { "x" | "twitter" => twitter::handler(&self.ctx, &caps).await, - "instagram" => instagram::handler(&self.ctx, &caps).await, + // "instagram" => instagram::handler(&self.ctx, &caps).await, + "instagram" => unimplemented!(), "facebook" => unimplemented!(), "threads" => unimplemented!(), "youtube" => unimplemented!(), From 188d6422243c25b637a1047c238e6f63c0e9c3be Mon Sep 17 00:00:00 2001 From: LTurret Date: Wed, 29 Apr 2026 00:37:45 +0800 Subject: [PATCH 15/22] =?UTF-8?q?=F0=9F=92=A5=20Remove=20redundant=20proce?= =?UTF-8?q?dure=20and=20Threads,=20Instragram=20patched?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/author.rs | 13 +++-- src/commands/embed.rs | 61 +++++++++++++++++---- src/commands/instagram.rs | 56 +++++++++---------- src/commands/mod.rs | 2 +- src/commands/patcher.rs | 83 ---------------------------- src/commands/threads.rs | 111 ++++++++++++++++++++++++++++++++++++++ src/commands/twitter.rs | 36 +++++-------- src/main.rs | 15 ++++-- 8 files changed, 222 insertions(+), 155 deletions(-) delete mode 100644 src/commands/patcher.rs create mode 100644 src/commands/threads.rs diff --git a/src/commands/author.rs b/src/commands/author.rs index 4a013b5..8019ebb 100644 --- a/src/commands/author.rs +++ b/src/commands/author.rs @@ -5,7 +5,7 @@ pub struct Author { pub url: String, pub name: String, pub screen_name: String, - pub icon_url: String, + pub icon_url: Option, } impl Author { @@ -18,16 +18,21 @@ impl Author { url: get_string(json_data, "url"), name: get_string(json_data, "name"), screen_name: get_string(json_data, "screen_name"), - icon_url: get_string(json_data, "avatar_url"), + icon_url: json_data["avatar_url"].as_str().map(|s| s.to_string()), } } - pub fn from_str(url: &String, name: &String, screen_name: &String, icon_url: &String) -> Self { + pub fn from_str( + url: &String, + name: &String, + screen_name: &String, + icon_url: Option, + ) -> Self { Self { url: String::from(url), name: String::from(name), screen_name: String::from(screen_name), - icon_url: String::from(icon_url), + icon_url, } } } diff --git a/src/commands/embed.rs b/src/commands/embed.rs index 51cd268..fb9ac07 100644 --- a/src/commands/embed.rs +++ b/src/commands/embed.rs @@ -1,35 +1,78 @@ -use crate::commands::{instagram::InstagramFetcher, twitter::TweetFetcher}; +use crate::commands::{instagram::InstagramBuilder, threads::ThreadBuilder, twitter::TweetBuilder}; use async_trait::async_trait; use regex::Captures; -use serenity::{builder::CreateMessage, prelude::*}; +use reqwest::Client as HttpClient; +use serde_json::json; +use serenity::{builder::CreateMessage, http::Typing, model::channel::Message, prelude::*}; +use std::env; +use tokio::time::{sleep, Duration}; pub struct Embed; #[async_trait] -pub trait ContentFetcher: Send + Sync { +pub trait ContentBuilder: Send + Sync { async fn embed_message(&self, endpoint: &str, ctx: &Context) -> CreateMessage; } impl Embed { - pub async fn new_embed(&self, ctx: &Context, caps: &Captures<'_>) -> CreateMessage { + async fn suppress_original_embed(msg: &Message) -> () { + let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + sleep(Duration::from_millis(250)).await; + + let url: String = format!( + "https://discord.com/api/v9/channels/{}/messages/{}", + &msg.channel_id, &msg.id + ); + + let client: HttpClient = HttpClient::new(); + client + .patch(&url) + .header("accept", "*/*") + .header("authorization", format!("Bot {}", token)) + .header("content-type", "application/json") + .body( + json!({ + "flags": 4 + }) + .to_string(), + ) + .send() + .await + .ok(); + } + + pub async fn process_url(ctx: &Context, msg: &Message, caps: &Captures<'_>) -> () { + let typing = Typing::start(ctx.http.clone(), msg.channel_id); + + let embed_message = Self::new_embed(ctx, caps).await; + if let Err(why) = msg.channel_id.send_message(&ctx.http, embed_message).await { + eprintln!("Error sending message: {why:?}"); + } + + Self::suppress_original_embed(msg).await; + typing.stop(); + } + + pub async fn new_embed(ctx: &Context, caps: &Captures<'_>) -> CreateMessage { let endpoint: &str = caps .name("endpoint") .expect("Expected a valid haystack") .as_str(); // Regex Pattern: (http|https)://(?.+)\.com(?(/.+)*) - let fetcher: Box = match caps + let fetcher: Box = match caps .name("domain") - .expect("Expacted a valid haystack") + .expect("Expected a valid haystack") .as_str() { // ContentFetcher selector by domain - "x" | "twitter" => Box::new(TweetFetcher), - "instagram" => Box::new(InstagramFetcher), + "x" | "twitter" => Box::new(TweetBuilder), + "threads" => Box::new(ThreadBuilder), + "instagram" => Box::new(InstagramBuilder), _ => unimplemented!(), }; - let embed_message: CreateMessage = fetcher.embed_message(&endpoint, ctx).await; + let embed_message: CreateMessage = fetcher.embed_message(endpoint, ctx).await; embed_message } diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 68efd60..10b89bc 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -1,15 +1,8 @@ -use crate::commands::{ - author::Author, - embed::{ContentFetcher, Embed}, -}; +use crate::commands::{author::Author, embed::ContentBuilder}; use async_trait::async_trait; use html_escape::decode_html_entities; -use regex::{Captures, Regex}; -use reqwest::{ - header::{HeaderMap, HeaderValue, USER_AGENT}, - Client as HttpClient, -}; -use serde_json::Value; +use regex::Regex; +use reqwest::{header::USER_AGENT, Client as HttpClient}; use serenity::{ builder::{ CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, @@ -38,8 +31,8 @@ impl InstagramPost { .as_str() .to_string(); - let url: &String = &String::from(format!("https://www.instagram.com/{}", &author)); - let icon_url: String = InstagramPost::get_profile_pic_url(&author).await; + let url: &String = &format!("https://www.instagram.com/{}", &author); + // let icon_url: String = InstagramPost::get_profile_pic_url(&author).await; let raw_content: &str = Regex::new( r#"(?s).+)".+> String { let mut headers = HeaderMap::new(); headers.insert("x-ig-app-id", HeaderValue::from_static("936619743392459")); @@ -124,10 +119,16 @@ impl InstagramPost { }) .expect("csrftoken not found"); - let body = format!("variables=%7B%22enable_integrity_filters%22%3Atrue%2C%22id%22%3A%22{}%22%2C%22render_surface%22%3A%22PROFILE%22%2C%22__relay_internal__pv__PolarisCannesGuardianExperienceEnabledrelayprovider%22%3Atrue%2C%22__relay_internal__pv__PolarisCASB976ProfileEnabledrelayprovider%22%3Afalse%2C%22__relay_internal__pv__PolarisRepostsConsumptionEnabledrelayprovider%22%3Afalse%7D&doc_id=25980296051578533", user_id); + let body = format!( + "variables=%7B%22enable_integrity_filters%22%3Atrue%2C%22id%22%3A%22{}%22%2C%22render_surface%22%3A%22PROFILE%22%2C%22__relay_internal__pv__PolarisCannesGuardianExperienceEnabledrelayprovider%22%3Atrue%2C%22__relay_internal__pv__PolarisCASB976ProfileEnabledrelayprovider%22%3Afalse%2C%22__relay_internal__pv__PolarisRepostsConsumptionEnabledrelayprovider%22%3Afalse%7D&doc_id=25980296051578533", + user_id + ); let mut headers = HeaderMap::new(); - let cookie = format!("ig_did=B9C9BB5D-2753-46D0-9784-3C94B0FAD0C9;csrftoken={};datr=rl40aad9n6XXVIcGcEsaMfZU;mid=aTRergALAAGAj_Wk-MQWM3oJJrI3;ps_l=1; ps_n=1; ig_nrcb=1; wd=958x944", csrftoken); + let cookie = format!( + "ig_did=B9C9BB5D-2753-46D0-9784-3C94B0FAD0C9;csrftoken={};datr=rl40aad9n6XXVIcGcEsaMfZU;mid=aTRergALAAGAj_Wk-MQWM3oJJrI3;ps_l=1; ps_n=1; ig_nrcb=1; wd=958x944", + csrftoken + ); headers.insert( HeaderName::from_static("cookie"), @@ -158,18 +159,17 @@ impl InstagramPost { } } - let response = String::from("gay"); response } - async fn to_embed(self) -> CreateMessage { + async fn into_embed(self) -> CreateMessage { let embed: CreateEmbed = CreateEmbed::new() .color(Color::new(0xce0071)) - .author(CreateEmbedAuthor::new(format!("{}", self.author.name)).icon_url(self.author.icon_url).url(self.author.url)) + .author(CreateEmbedAuthor::new(self.author.name).url(self.author.url)) .description(self.content) .footer( CreateEmbedFooter::new("Instagram") - .icon_url("https://images-ext-1.discordapp.net/external/C6jCIKlXguRhfmSp6USkbWsS11fnsbBgMXiclR2R4ps/https/www.instagram.com/static/images/ico/favicon-192.png/68d99ba29cc8.png"), + .icon_url("https://cdn-icons-png.flaticon.com/512/15707/15707749.png"), ) .url("https://lturret.xyz"); @@ -182,21 +182,20 @@ impl InstagramPost { } } -pub struct InstagramFetcher; +pub struct InstagramBuilder; #[async_trait] -impl ContentFetcher for InstagramFetcher { +impl ContentBuilder for InstagramBuilder { async fn embed_message(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { let clean_endpoint = format!( "https://www.instagram.com/p/{}/", - Regex::new(r"\/p\/(?.+)\/") + Regex::new(r"/p/(?.+)/?") .expect("Expected a valid regex pattern") .captures(endpoint) .expect("Expected a valid haystack") .name("post_id") .expect("Expected a valid matching") .as_str() - .to_string() ); let response_result: Result = HttpClient::new() @@ -217,12 +216,7 @@ impl ContentFetcher for InstagramFetcher { }; let instagram_post: InstagramPost = InstagramPost::from_raw_response(response).await; - let embed_message: CreateMessage = instagram_post.to_embed().await; + let embed_message: CreateMessage = instagram_post.into_embed().await; embed_message } } - -pub async fn handler(ctx: &Context, caps: &Captures<'_>) -> CreateMessage { - let embed_message = Embed.new_embed(ctx, caps).await; - embed_message -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 37c20c0..13f0d78 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,5 +1,5 @@ pub mod author; pub mod embed; pub mod instagram; -pub mod patcher; +pub mod threads; pub mod twitter; diff --git a/src/commands/patcher.rs b/src/commands/patcher.rs deleted file mode 100644 index 030f2ee..0000000 --- a/src/commands/patcher.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::commands::{instagram, twitter}; -use regex::{Captures, Regex}; -use reqwest::Client as HttpClient; -use serde_json::json; -use serenity::{builder::CreateMessage, http::Typing, model::channel::Message, prelude::*}; -use std::env; -use tokio::time::{sleep, Duration}; - -pub struct Patcher { - ctx: Context, - msg: Message, -} - -impl Patcher { - pub fn new(ctx: Context, msg: Message) -> Self { - Self { ctx: ctx, msg: msg } - } - - pub async fn parse(&self) -> () { - let typing = Typing::start(self.ctx.http.clone(), self.msg.channel_id); - - let caps: Captures = - Regex::new(r"(http|https)://(www\.)*(?.+)\.(cc|com)(?(/.+)*)") - .expect("Regex syntax inv`alid") - .captures(&self.msg.content) - .expect("Expected a valid haystack"); - - // Decide domain - let embed_message: CreateMessage = match caps - .name("domain") - .expect("Expected domain in haystack") - .as_str() - { - "x" | "twitter" => twitter::handler(&self.ctx, &caps).await, - // "instagram" => instagram::handler(&self.ctx, &caps).await, - "instagram" => unimplemented!(), - "facebook" => unimplemented!(), - "threads" => unimplemented!(), - "youtube" => unimplemented!(), - "ptt" => unimplemented!(), - _ => unimplemented!(), - }; - - // Sending embed message - if let Err(why) = self - .msg - .channel_id - .send_message(&self.ctx.http, embed_message) - .await - { - eprintln!("Error sending message: {why:?}"); - } - - let _ = self.remove_old_embed().await; - typing.stop(); - } - - async fn remove_old_embed(&self) -> () { - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - sleep(Duration::from_millis(250)).await; - - let url: String = format!( - "https://discord.com/api/v9/channels/{}/messages/{}", - &self.msg.channel_id, &self.msg.id - ); - - let client: HttpClient = HttpClient::new(); - client - .patch(&url) - .header("accept", "*/*") - .header("authorization", format!("Bot {}", token)) - .header("content-type", "application/json") - .body( - json!({ - "flags": 4 - }) - .to_string(), - ) - .send() - .await - .ok(); - } -} diff --git a/src/commands/threads.rs b/src/commands/threads.rs new file mode 100644 index 0000000..0297bb1 --- /dev/null +++ b/src/commands/threads.rs @@ -0,0 +1,111 @@ +use crate::commands::{author::Author, embed::ContentBuilder}; +use async_trait::async_trait; +use html_escape::decode_html_entities; +use regex::Regex; +use reqwest::{header::USER_AGENT, Client as HttpClient}; +use serenity::{ + builder::{ + CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, + }, + model::Color, + prelude::*, +}; + +#[derive(Debug)] +pub struct Thread { + pub author: Author, + pub content: String, + pub videos_supplementary: String, +} + +impl Thread { + async fn from_raw_response(raw_response: String) -> Self { + let author: String = + Regex::new(r"https://www\.threads\.com/(?@[a-zA-Z0-9._-]+)/") + .expect("Regex syntax invalid") + .captures(&raw_response) + .expect("Expected a valid haystack") + .name("author") + .unwrap() + .as_str() + .to_string(); + + let decoded_author_name: String = decode_html_entities(&author).to_string(); + let url: &String = &format!("https://www.threads.com/{}", decoded_author_name); + let raw_content: &str = Regex::new(r"(?<content>[\s\S]+)") + .expect("Regex syntax invalid") + .captures(&raw_response) + .expect("Expected a valid haystack") + .name("content") + .unwrap() + .as_str(); + + let content_chars: Vec = decode_html_entities(&raw_content).chars().collect(); + let content: String = content_chars[2..content_chars.len()].iter().collect(); + let videos_supplementary: String = String::from(""); + + Self { + author: Author::from_str(url, &decoded_author_name, &decoded_author_name, None), + content, + videos_supplementary, + } + } + + async fn into_embed(self) -> CreateMessage { + let embed: CreateEmbed = CreateEmbed::new() + .color(Color::new(0x181818)) + .author(CreateEmbedAuthor::new(self.author.name).url(self.author.url)) + .description(self.content) + .footer( + CreateEmbedFooter::new("Threads") + .icon_url("https://cdn-icons-png.flaticon.com/512/12105/12105338.png"), + ) + .url("https://lturret.xyz"); + + let builder: CreateMessage = CreateMessage::new() + .content(&self.videos_supplementary) + .allowed_mentions(CreateAllowedMentions::new().empty_users()) + .embed(embed); + + builder + } +} + +pub struct ThreadBuilder; + +#[async_trait] +impl ContentBuilder for ThreadBuilder { + async fn embed_message(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { + let clean_endpoint = format!( + "https://www.threads.com/{}/", + Regex::new(r"(?@.+/post/.+)") + .expect("Expected a valid regex pattern") + .captures(endpoint) + .expect("Expected a valid haystack") + .name("thread_endpoint") + .expect("Expected a valid matching") + .as_str() + ); + + let response_result: Result = HttpClient::new() + .get(clean_endpoint) + .header( + USER_AGENT, + "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)", + ) + .send() + .await; + + let response: String = match response_result { + Ok(res) => res.text().await.expect("Failed to read response text"), + Err(e) => { + eprintln!("{}", e); + return CreateMessage::new().content("Failed to fetch post"); + } + }; + + let thread_post: Thread = Thread::from_raw_response(response).await; + let embed_message: CreateMessage = thread_post.into_embed().await; + embed_message + } +} diff --git a/src/commands/twitter.rs b/src/commands/twitter.rs index fa85d34..2e344f5 100644 --- a/src/commands/twitter.rs +++ b/src/commands/twitter.rs @@ -1,7 +1,4 @@ -use crate::commands::{ - author::Author, - embed::{ContentFetcher, Embed}, -}; +use crate::commands::{author::Author, embed::ContentBuilder}; use async_trait::async_trait; use regex::{Captures, Regex}; use reqwest::{header::USER_AGENT, Client as HttpClient}; @@ -52,7 +49,7 @@ impl Tweet { CreateEmbed::new().url("https://lturret.xyz").image( Regex::new(r"(?https://pbs.twimg.com/media/.+\.jpg)(\?.+)*") .expect("Expected a valid regex") - .captures(&obj["url"].as_str().unwrap_or("")) + .captures(obj["url"].as_str().unwrap_or("")) .expect("Expected a valid haystack") .name("image_cdn_url") .expect("Expected a valid matchig") @@ -90,22 +87,22 @@ impl Tweet { } let mut videos_supplementary: String = String::new(); - let _ = raw_videos.iter().enumerate().for_each(|(i, url)| { + raw_videos.iter().enumerate().for_each(|(i, url)| { videos_supplementary .push_str(format!("-# [推文影片連結 {}]({})\n", i + 1, url).as_str()) }); Self { author: Author::from_json(&json_api_data["tweet"]["author"]), - content: content, - timestamp: timestamp, - images: images, - videos: videos, - videos_supplementary: videos_supplementary, + content, + timestamp, + images, + videos, + videos_supplementary, } } - async fn to_embed(self) -> CreateMessage { + async fn into_embed(self) -> CreateMessage { let embed: CreateEmbed = CreateEmbed::new() .color(Color::new(0x00b0f4)) .author( @@ -113,7 +110,7 @@ impl Tweet { "{}(@{})", self.author.name, self.author.screen_name )) - .icon_url(self.author.icon_url) + .icon_url(self.author.icon_url.unwrap()) .url(self.author.url), ) .description(self.content) @@ -139,10 +136,10 @@ impl Tweet { } } -pub struct TweetFetcher; +pub struct TweetBuilder; #[async_trait] -impl ContentFetcher for TweetFetcher { +impl ContentBuilder for TweetBuilder { async fn embed_message(&self, endpoint: &str, ctx: &Context) -> CreateMessage { let caps: Captures<'_> = Regex::new(r"(?/.+/status/[0-9]+)(\?.=.+)*") .expect("Expected a valid regex pattern") @@ -175,13 +172,8 @@ impl ContentFetcher for TweetFetcher { }; let api_json = response.text().await.expect("Failed to read response text"); - let tweet: Tweet = Tweet::from_raw_api(&ctx, api_json).await; - let embed_message: CreateMessage = tweet.to_embed().await; + let tweet: Tweet = Tweet::from_raw_api(ctx, api_json).await; + let embed_message: CreateMessage = tweet.into_embed().await; embed_message } } - -pub async fn handler(ctx: &Context, caps: &Captures<'_>) -> CreateMessage { - let embed_message = Embed.new_embed(ctx, caps).await; - embed_message -} diff --git a/src/main.rs b/src/main.rs index eb84b87..44bfb18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ mod commands; -use crate::commands::patcher::Patcher; +use crate::commands::embed::Embed; use dotenv::dotenv; use regex::Regex; use serenity::{ @@ -14,10 +14,15 @@ struct Handler; #[async_trait] impl EventHandler for Handler { async fn message(&self, ctx: Context, msg: Message) { - let re: Regex = Regex::new(r"(http|https)://(www\.)*(?(x|twitter|instagram|facebook|threads|ppt)\.(cc|com))") - .expect("Regex syntax invalid"); - if msg.author.id != UserId::new(1441446989362626772) && re.is_match(&msg.content) { - Patcher::new(ctx, msg).parse().await; + let re: Regex = Regex::new( + r"(http|https)://(www\.)*(?(instagram|twitter|threads|x))\.(cc|com)(?(/.+)*)", + ) + .expect("Regex syntax invalid"); + + if let Some(caps) = re.captures(&msg.content) + && msg.author.id != UserId::new(1441446989362626772) + { + let _ = Embed::process_url(&ctx, &msg, &caps).await; } } From d00ee426cd9fc577328deab1d5a6883aaf684b92 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 3 May 2026 01:43:12 +0800 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=90=9B=20Fix=20URL=20matching=20syn?= =?UTF-8?q?tax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/threads.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/threads.rs b/src/commands/threads.rs index 0297bb1..5e6f73e 100644 --- a/src/commands/threads.rs +++ b/src/commands/threads.rs @@ -76,9 +76,9 @@ pub struct ThreadBuilder; #[async_trait] impl ContentBuilder for ThreadBuilder { async fn embed_message(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { - let clean_endpoint = format!( - "https://www.threads.com/{}/", - Regex::new(r"(?@.+/post/.+)") + let clean_url = format!( + "https://www.threads.com/{}", + Regex::new(r"(?@.+/post/[\w]+)/?") .expect("Expected a valid regex pattern") .captures(endpoint) .expect("Expected a valid haystack") From 44e9adcbacab406631aad30d7b31df7b5312f0ed Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 3 May 2026 03:40:08 +0800 Subject: [PATCH 17/22] =?UTF-8?q?=F0=9F=8E=A8=20Formats=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check_formatting.yml | 32 +++++++++++++++ rustfmt.toml | 6 +++ src/commands/embed.rs | 41 +++++++++++++------ src/commands/instagram.rs | 49 +++++++++++++--------- src/commands/threads.rs | 56 ++++++++++++++++++-------- src/main.rs | 6 +-- 6 files changed, 138 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/check_formatting.yml create mode 100644 rustfmt.toml diff --git a/.github/workflows/check_formatting.yml b/.github/workflows/check_formatting.yml new file mode 100644 index 0000000..88e86cd --- /dev/null +++ b/.github/workflows/check_formatting.yml @@ -0,0 +1,32 @@ +name: check_formatting.yml + +on: + push: + branches: [main] + pull_request: + +jobs: + rust: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust (nightly) + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt, clippy + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Format check + run: cargo fmt -- --check + + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Test + run: cargo test --all-features diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..9eb5380 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ +max_width = 80 +tab_spaces = 4 +use_small_heuristics = "Max" +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +reorder_imports = true diff --git a/src/commands/embed.rs b/src/commands/embed.rs index fb9ac07..e4c9703 100644 --- a/src/commands/embed.rs +++ b/src/commands/embed.rs @@ -1,9 +1,13 @@ -use crate::commands::{instagram::InstagramBuilder, threads::ThreadBuilder, twitter::TweetBuilder}; +use crate::commands::{ + instagram::InstagramBuilder, threads::ThreadBuilder, twitter::TweetBuilder, +}; use async_trait::async_trait; use regex::Captures; use reqwest::Client as HttpClient; use serde_json::json; -use serenity::{builder::CreateMessage, http::Typing, model::channel::Message, prelude::*}; +use serenity::{ + builder::CreateMessage, http::Typing, model::channel::Message, prelude::*, +}; use std::env; use tokio::time::{sleep, Duration}; @@ -11,12 +15,17 @@ pub struct Embed; #[async_trait] pub trait ContentBuilder: Send + Sync { - async fn embed_message(&self, endpoint: &str, ctx: &Context) -> CreateMessage; + async fn embed_message( + &self, + endpoint: &str, + ctx: &Context, + ) -> CreateMessage; } impl Embed { async fn suppress_original_embed(msg: &Message) -> () { - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = env::var("DISCORD_TOKEN") + .expect("Expected a token in the environment"); sleep(Duration::from_millis(250)).await; let url: String = format!( @@ -41,11 +50,17 @@ impl Embed { .ok(); } - pub async fn process_url(ctx: &Context, msg: &Message, caps: &Captures<'_>) -> () { + pub async fn process_url( + ctx: &Context, + msg: &Message, + caps: &Captures<'_>, + ) -> () { let typing = Typing::start(ctx.http.clone(), msg.channel_id); let embed_message = Self::new_embed(ctx, caps).await; - if let Err(why) = msg.channel_id.send_message(&ctx.http, embed_message).await { + if let Err(why) = + msg.channel_id.send_message(&ctx.http, embed_message).await + { eprintln!("Error sending message: {why:?}"); } @@ -53,11 +68,12 @@ impl Embed { typing.stop(); } - pub async fn new_embed(ctx: &Context, caps: &Captures<'_>) -> CreateMessage { - let endpoint: &str = caps - .name("endpoint") - .expect("Expected a valid haystack") - .as_str(); + pub async fn new_embed( + ctx: &Context, + caps: &Captures<'_>, + ) -> CreateMessage { + let endpoint: &str = + caps.name("endpoint").expect("Expected a valid haystack").as_str(); // Regex Pattern: (http|https)://(?.+)\.com(?(/.+)*) let fetcher: Box = match caps @@ -72,7 +88,8 @@ impl Embed { _ => unimplemented!(), }; - let embed_message: CreateMessage = fetcher.embed_message(endpoint, ctx).await; + let embed_message: CreateMessage = + fetcher.embed_message(endpoint, ctx).await; embed_message } diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 10b89bc..5c6d9b4 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -5,7 +5,8 @@ use regex::Regex; use reqwest::{header::USER_AGENT, Client as HttpClient}; use serenity::{ builder::{ - CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, + CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, + CreateEmbedFooter, CreateMessage, }, model::Color, prelude::*, @@ -44,8 +45,10 @@ impl InstagramPost { .unwrap() .as_str(); - let content_chars: Vec = decode_html_entities(&raw_content).chars().collect(); - let content: String = content_chars[2..content_chars.len() - 1].iter().collect(); + let content_chars: Vec = + decode_html_entities(&raw_content).chars().collect(); + let content: String = + content_chars[2..content_chars.len() - 1].iter().collect(); let videos_supplementary: String = String::from(""); Self { @@ -59,7 +62,8 @@ impl InstagramPost { #[cfg(false)] async fn get_profile_pic_url(username: &String) -> String { let mut headers = HeaderMap::new(); - headers.insert("x-ig-app-id", HeaderValue::from_static("936619743392459")); + headers + .insert("x-ig-app-id", HeaderValue::from_static("936619743392459")); let response = reqwest::Client::new() .get(format!( @@ -103,7 +107,12 @@ impl InstagramPost { let cookie_val: HeaderValue = jar .as_ref() - .cookies(&Url::parse(format!("https://www.instagram.com/{}", author).as_str()).unwrap()) + .cookies( + &Url::parse( + format!("https://www.instagram.com/{}", author).as_str(), + ) + .unwrap(), + ) .ok_or("no cookies found") .unwrap(); @@ -112,10 +121,7 @@ impl InstagramPost { .split(';') .find(|c| c.trim().starts_with("csrftoken=")) .map(|c| { - c.trim() - .strip_prefix("csrftoken=") - .unwrap_or("") - .to_string() + c.trim().strip_prefix("csrftoken=").unwrap_or("").to_string() }) .expect("csrftoken not found"); @@ -165,12 +171,13 @@ impl InstagramPost { async fn into_embed(self) -> CreateMessage { let embed: CreateEmbed = CreateEmbed::new() .color(Color::new(0xce0071)) - .author(CreateEmbedAuthor::new(self.author.name).url(self.author.url)) - .description(self.content) - .footer( - CreateEmbedFooter::new("Instagram") - .icon_url("https://cdn-icons-png.flaticon.com/512/15707/15707749.png"), + .author( + CreateEmbedAuthor::new(self.author.name).url(self.author.url), ) + .description(self.content) + .footer(CreateEmbedFooter::new("Instagram").icon_url( + "https://cdn-icons-png.flaticon.com/512/15707/15707749.png", + )) .url("https://lturret.xyz"); let builder: CreateMessage = CreateMessage::new() @@ -186,7 +193,11 @@ pub struct InstagramBuilder; #[async_trait] impl ContentBuilder for InstagramBuilder { - async fn embed_message(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { + async fn embed_message( + &self, + endpoint: &str, + _ctx: &Context, + ) -> CreateMessage { let clean_endpoint = format!( "https://www.instagram.com/p/{}/", Regex::new(r"/p/(?.+)/?") @@ -200,10 +211,7 @@ impl ContentBuilder for InstagramBuilder { let response_result: Result = HttpClient::new() .get(clean_endpoint) - .header( - USER_AGENT, - "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)", - ) + .header(USER_AGENT, "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)") .send() .await; @@ -215,7 +223,8 @@ impl ContentBuilder for InstagramBuilder { } }; - let instagram_post: InstagramPost = InstagramPost::from_raw_response(response).await; + let instagram_post: InstagramPost = + InstagramPost::from_raw_response(response).await; let embed_message: CreateMessage = instagram_post.into_embed().await; embed_message } diff --git a/src/commands/threads.rs b/src/commands/threads.rs index 5e6f73e..596bf99 100644 --- a/src/commands/threads.rs +++ b/src/commands/threads.rs @@ -15,7 +15,6 @@ use serenity::{ pub struct Thread { pub author: Author, pub content: String, - pub videos_supplementary: String, } impl Thread { @@ -26,35 +25,60 @@ impl Thread { .captures(&raw_response) .expect("Expected a valid haystack") .name("author") - .unwrap() + .expect("String not match") .as_str() .to_string(); let decoded_author_name: String = decode_html_entities(&author).to_string(); let url: &String = &format!("https://www.threads.com/{}", decoded_author_name); - let raw_content: &str = Regex::new(r"(?<content>[\s\S]+)") + let profile: String = HttpClient::new() + .get(url) + .header( + USER_AGENT, + "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)", + ) + .send() + .await + .expect("Connection error") + .text() + .await + .expect("Failed to read response text"); + + let author_alias: String = + Regex::new(r"(?<author_alias>.+)\s\(@.+\)\s.\s.+") + .expect("Regex syntax invalid") + .captures(&profile) + .expect("Expected a valid haystack") + .name("author_alias") + .expect("String not match") + .as_str() + .to_string(); + + let content: String = Regex::new(r"(?<content>[\s\S]+)") .expect("Regex syntax invalid") .captures(&raw_response) .expect("Expected a valid haystack") .name("content") - .unwrap() - .as_str(); - - let content_chars: Vec = decode_html_entities(&raw_content).chars().collect(); - let content: String = content_chars[2..content_chars.len()].iter().collect(); - let videos_supplementary: String = String::from(""); + .expect("String not match") + .as_str() + .to_string(); Self { - author: Author::from_str(url, &decoded_author_name, &decoded_author_name, None), + author: Author::from_str(url, &decoded_author_name, &author_alias, None), content, - videos_supplementary, } } async fn into_embed(self) -> CreateMessage { let embed: CreateEmbed = CreateEmbed::new() .color(Color::new(0x181818)) - .author(CreateEmbedAuthor::new(self.author.name).url(self.author.url)) + .author( + CreateEmbedAuthor::new(format!( + "{} ({})", + self.author.screen_name, self.author.name + )) + .url(self.author.url), + ) .description(self.content) .footer( CreateEmbedFooter::new("Threads") @@ -63,7 +87,6 @@ impl Thread { .url("https://lturret.xyz"); let builder: CreateMessage = CreateMessage::new() - .content(&self.videos_supplementary) .allowed_mentions(CreateAllowedMentions::new().empty_users()) .embed(embed); @@ -88,7 +111,7 @@ impl ContentBuilder for ThreadBuilder { ); let response_result: Result = HttpClient::new() - .get(clean_endpoint) + .get(clean_url) .header( USER_AGENT, "Rust Discord Bot (https://github.com/LTurret/ArisaMatsuda)", @@ -104,8 +127,9 @@ impl ContentBuilder for ThreadBuilder { } }; - let thread_post: Thread = Thread::from_raw_response(response).await; - let embed_message: CreateMessage = thread_post.into_embed().await; + let embed_message: CreateMessage = + Thread::from_raw_response(response).await.into_embed().await; + embed_message } } diff --git a/src/main.rs b/src/main.rs index 44bfb18..f26e8d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,10 +41,8 @@ async fn main() { | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) - .event_handler(Handler) - .await - .expect("Err creating client"); + let mut client = + Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); if let Err(why) = client.start().await { eprintln!("Client error: {why:?}"); From c92c531a37313560e02c7c348d6a2852ec88d584 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 3 May 2026 03:43:31 +0800 Subject: [PATCH 18/22] =?UTF-8?q?=F0=9F=93=9D=20Update=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 +---------------- README_zh-TW.md | 83 ------------------------------------------------- 2 files changed, 1 insertion(+), 111 deletions(-) delete mode 100644 README_zh-TW.md diff --git a/README.md b/README.md index deef6d9..2574dbd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # ArisaMatsuda -English|[繁體中文](./README_zh-TW.md) - -A discord bot for private server management. +A discord bot for help domain content embedding. ## Configuration @@ -53,31 +51,6 @@ cargo build --release ./target/release/arisa_rust & ``` -- Docker: - - ```sh - wip - ``` - -## Roadmap - -> [!NOTE] -> Some of features are removed from the list since the last Python build. -> Please refer to the main branch for more information. - -There would be more tracks in the project tab! - -| **File** | **Status** | -|------------------|------------------| -| **tweet_fix.py** | Work in Progress | -| _emotes.py_ | Not implemented | -| _goods.py_ | Not implemented | -| _ping.py_ | Not implemented | -| fun.py | Not implemented | -| tweet_subscribe | Not implemented | -| ~~delete.py~~ | Despreated | -| ~~join.py~~ | Despreated | - ## License Licensed under [MIT](LICENSE). diff --git a/README_zh-TW.md b/README_zh-TW.md deleted file mode 100644 index fb006ec..0000000 --- a/README_zh-TW.md +++ /dev/null @@ -1,83 +0,0 @@ -# ArisaMatsuda - -[English](./README.md)|繁體中文 - -私人伺服器管理用 Discord 機器人。 - -## 設定 - -在直接從此儲存庫執行機器人之前,需要完成一些必要的步驟,否則機器人將無法正常運行。 - -### 目錄結構 - -```plain -ArisaMatsuda/ -├── src -│   └── main.rs -├── Cargo.toml -├── LICENSE -├── README.md -└── README_zh-TW.md -``` - -### 機密資訊與設定 - -機器人權杖(Token)使用 `dotenv::dotenv.ok()` 和 `std::env::var()` 存取。 - -`.env` 檔案應包含以下設定: - -```env -# 必需的設定 -DISCORD_TOKEN="" -``` - -請確保將 plcaeholders 替換為你的實際值。 - -## 建置 - -```sh -cargo build --release -``` - -### 執行 - -- npm/pm2: - - ```sh - pm2 start target/release/arisa_rust --name "arisa" --update-env - ``` - -- Background job: - - ```sh - ./target/release/arisa_rust & - ``` - -- Docker: - - ```sh - wip - ``` - -## 規劃路線圖 - -> [!NOTE] -> 某些功能在之前的 Python 版本中被規劃移除,因此不列入以下表中。 -> 請參照主分支以獲得更多資訊。 - -接下來會再 project 中有更詳細的規劃! - -| **File** | **Status** | -|------------------|------------| -| **tweet_fix.py** | 實作中 | -| _emotes.py_ | 尚未實作 | -| _goods.py_ | 尚未實作 | -| _ping.py_ | 尚未實作 | -| fun.py | 尚未實作 | -| tweet_subscribe | 尚未實作 | -| ~~delete.py~~ | 已棄用 | -| ~~join.py~~ | 已棄用 | - -## 授權 - -本專案採用 [MIT](LICENSE) 授權。 From 92a487769a8493757710b6ecb9bcb069068240e0 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 3 May 2026 03:44:34 +0800 Subject: [PATCH 19/22] =?UTF-8?q?=F0=9F=8E=A8=20Formats=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rustfmt.toml | 2 -- src/commands/embed.rs | 2 +- src/commands/instagram.rs | 4 +-- src/commands/threads.rs | 69 +++++++++++++++++++++++---------------- src/commands/twitter.rs | 57 +++++++++++++++++--------------- src/main.rs | 9 +++-- 6 files changed, 81 insertions(+), 62 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index 9eb5380..f560266 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,6 +1,4 @@ max_width = 80 tab_spaces = 4 use_small_heuristics = "Max" -imports_granularity = "Crate" -group_imports = "StdExternalCrate" reorder_imports = true diff --git a/src/commands/embed.rs b/src/commands/embed.rs index e4c9703..63f2d64 100644 --- a/src/commands/embed.rs +++ b/src/commands/embed.rs @@ -9,7 +9,7 @@ use serenity::{ builder::CreateMessage, http::Typing, model::channel::Message, prelude::*, }; use std::env; -use tokio::time::{sleep, Duration}; +use tokio::time::{Duration, sleep}; pub struct Embed; diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 5c6d9b4..48a1dc2 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -2,7 +2,7 @@ use crate::commands::{author::Author, embed::ContentBuilder}; use async_trait::async_trait; use html_escape::decode_html_entities; use regex::Regex; -use reqwest::{header::USER_AGENT, Client as HttpClient}; +use reqwest::{Client as HttpClient, header::USER_AGENT}; use serenity::{ builder::{ CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, @@ -87,9 +87,9 @@ impl InstagramPost { #[cfg(false)] async fn get_icon_url(author: &String) -> String { use reqwest::{ + Url, cookie::{CookieStore, Jar}, header::HeaderName, - Url, }; use std::sync::Arc; diff --git a/src/commands/threads.rs b/src/commands/threads.rs index 596bf99..6c33874 100644 --- a/src/commands/threads.rs +++ b/src/commands/threads.rs @@ -2,10 +2,11 @@ use crate::commands::{author::Author, embed::ContentBuilder}; use async_trait::async_trait; use html_escape::decode_html_entities; use regex::Regex; -use reqwest::{header::USER_AGENT, Client as HttpClient}; +use reqwest::{Client as HttpClient, header::USER_AGENT}; use serenity::{ builder::{ - CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, + CreateAllowedMentions, CreateEmbed, CreateEmbedAuthor, + CreateEmbedFooter, CreateMessage, }, model::Color, prelude::*, @@ -19,18 +20,21 @@ pub struct Thread { impl Thread { async fn from_raw_response(raw_response: String) -> Self { - let author: String = - Regex::new(r"https://www\.threads\.com/(?@[a-zA-Z0-9._-]+)/") - .expect("Regex syntax invalid") - .captures(&raw_response) - .expect("Expected a valid haystack") - .name("author") - .expect("String not match") - .as_str() - .to_string(); + let author: String = Regex::new( + r"https://www\.threads\.com/(?@[a-zA-Z0-9._-]+)/", + ) + .expect("Regex syntax invalid") + .captures(&raw_response) + .expect("Expected a valid haystack") + .name("author") + .expect("String not match") + .as_str() + .to_string(); - let decoded_author_name: String = decode_html_entities(&author).to_string(); - let url: &String = &format!("https://www.threads.com/{}", decoded_author_name); + let decoded_author_name: String = + decode_html_entities(&author).to_string(); + let url: &String = + &format!("https://www.threads.com/{}", decoded_author_name); let profile: String = HttpClient::new() .get(url) .header( @@ -44,15 +48,16 @@ impl Thread { .await .expect("Failed to read response text"); - let author_alias: String = - Regex::new(r"(?<author_alias>.+)\s\(@.+\)\s.\s.+") - .expect("Regex syntax invalid") - .captures(&profile) - .expect("Expected a valid haystack") - .name("author_alias") - .expect("String not match") - .as_str() - .to_string(); + let author_alias: String = Regex::new( + r"(?<author_alias>.+)\s\(@.+\)\s.\s.+", + ) + .expect("Regex syntax invalid") + .captures(&profile) + .expect("Expected a valid haystack") + .name("author_alias") + .expect("String not match") + .as_str() + .to_string(); let content: String = Regex::new(r"(?<content>[\s\S]+)") .expect("Regex syntax invalid") @@ -64,7 +69,12 @@ impl Thread { .to_string(); Self { - author: Author::from_str(url, &decoded_author_name, &author_alias, None), + author: Author::from_str( + url, + &decoded_author_name, + &author_alias, + None, + ), content, } } @@ -80,10 +90,9 @@ impl Thread { .url(self.author.url), ) .description(self.content) - .footer( - CreateEmbedFooter::new("Threads") - .icon_url("https://cdn-icons-png.flaticon.com/512/12105/12105338.png"), - ) + .footer(CreateEmbedFooter::new("Threads").icon_url( + "https://cdn-icons-png.flaticon.com/512/12105/12105338.png", + )) .url("https://lturret.xyz"); let builder: CreateMessage = CreateMessage::new() @@ -98,7 +107,11 @@ pub struct ThreadBuilder; #[async_trait] impl ContentBuilder for ThreadBuilder { - async fn embed_message(&self, endpoint: &str, _ctx: &Context) -> CreateMessage { + async fn embed_message( + &self, + endpoint: &str, + _ctx: &Context, + ) -> CreateMessage { let clean_url = format!( "https://www.threads.com/{}", Regex::new(r"(?@.+/post/[\w]+)/?") diff --git a/src/commands/twitter.rs b/src/commands/twitter.rs index 2e344f5..2da4fa4 100644 --- a/src/commands/twitter.rs +++ b/src/commands/twitter.rs @@ -1,16 +1,16 @@ use crate::commands::{author::Author, embed::ContentBuilder}; use async_trait::async_trait; use regex::{Captures, Regex}; -use reqwest::{header::USER_AGENT, Client as HttpClient}; -use serde_json::{from_str, Value}; +use reqwest::{Client as HttpClient, header::USER_AGENT}; +use serde_json::{Value, from_str}; use serenity::{ builder::{ - CreateAllowedMentions, CreateAttachment, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, - CreateMessage, + CreateAllowedMentions, CreateAttachment, CreateEmbed, + CreateEmbedAuthor, CreateEmbedFooter, CreateMessage, }, model::{ - timestamp::{InvalidTimestamp, Timestamp}, Color, + timestamp::{InvalidTimestamp, Timestamp}, }, prelude::*, }; @@ -30,16 +30,15 @@ impl Tweet { let json_api_data: Value = from_str(raw_api_data.as_str()).expect("Expected a valid payload"); - let content: String = json_api_data["tweet"]["text"] - .as_str() - .unwrap_or("") - .to_string(); + let content: String = + json_api_data["tweet"]["text"].as_str().unwrap_or("").to_string(); - let timestamp: Result = Timestamp::from_unix_timestamp( - json_api_data["tweet"]["created_timestamp"] - .as_i64() - .unwrap_or(0), - ); + let timestamp: Result = + Timestamp::from_unix_timestamp( + json_api_data["tweet"]["created_timestamp"] + .as_i64() + .unwrap_or(0), + ); let images: Vec = json_api_data["tweet"]["media"]["photos"] .as_array() @@ -88,8 +87,9 @@ impl Tweet { let mut videos_supplementary: String = String::new(); raw_videos.iter().enumerate().for_each(|(i, url)| { - videos_supplementary - .push_str(format!("-# [推文影片連結 {}]({})\n", i + 1, url).as_str()) + videos_supplementary.push_str( + format!("-# [推文影片連結 {}]({})\n", i + 1, url).as_str(), + ) }); Self { @@ -114,10 +114,9 @@ impl Tweet { .url(self.author.url), ) .description(self.content) - .footer( - CreateEmbedFooter::new("Twitter (X)") - .icon_url("https://abs.twimg.com/icons/apple-touch-icon-192x192.png"), - ) + .footer(CreateEmbedFooter::new("Twitter (X)").icon_url( + "https://abs.twimg.com/icons/apple-touch-icon-192x192.png", + )) .url("https://lturret.xyz") .timestamp( self.timestamp @@ -140,11 +139,16 @@ pub struct TweetBuilder; #[async_trait] impl ContentBuilder for TweetBuilder { - async fn embed_message(&self, endpoint: &str, ctx: &Context) -> CreateMessage { - let caps: Captures<'_> = Regex::new(r"(?/.+/status/[0-9]+)(\?.=.+)*") - .expect("Expected a valid regex pattern") - .captures(endpoint) - .expect("Expected a valid haystack"); + async fn embed_message( + &self, + endpoint: &str, + ctx: &Context, + ) -> CreateMessage { + let caps: Captures<'_> = + Regex::new(r"(?/.+/status/[0-9]+)(\?.=.+)*") + .expect("Expected a valid regex pattern") + .captures(endpoint) + .expect("Expected a valid haystack"); let api_url: String = format!( "https://api.fxtwitter.com{}", @@ -171,7 +175,8 @@ impl ContentBuilder for TweetBuilder { } }; - let api_json = response.text().await.expect("Failed to read response text"); + let api_json = + response.text().await.expect("Failed to read response text"); let tweet: Tweet = Tweet::from_raw_api(ctx, api_json).await; let embed_message: CreateMessage = tweet.into_embed().await; embed_message diff --git a/src/main.rs b/src/main.rs index f26e8d0..f6befac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,14 +35,17 @@ impl EventHandler for Handler { async fn main() { dotenv().ok(); - let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token: String = + env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); let intents: GatewayIntents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + let mut client = Client::builder(&token, intents) + .event_handler(Handler) + .await + .expect("Err creating client"); if let Err(why) = client.start().await { eprintln!("Client error: {why:?}"); From cc4f3c4224db59fdee3a50b62c0bc736805434ac Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 3 May 2026 03:51:37 +0800 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=94=92=20.unwrap()=20free?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/instagram.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/instagram.rs b/src/commands/instagram.rs index 48a1dc2..4f9ace9 100644 --- a/src/commands/instagram.rs +++ b/src/commands/instagram.rs @@ -24,11 +24,11 @@ impl InstagramPost { let author: String = Regex::new( r#".+)\/p\/"#, ) - .unwrap() + .expect("Regex syntax invalid") .captures(&raw_response) - .unwrap() + .expect("Expected a valid haystack") .name("author") - .unwrap() + .expect("String not match") .as_str() .to_string(); @@ -38,11 +38,11 @@ impl InstagramPost { let raw_content: &str = Regex::new( r#"(?s).+)".+> = From 5fa825b7b970b5b1991abe6e0353a66128b1f131 Mon Sep 17 00:00:00 2001 From: LTurret Date: Sun, 3 May 2026 03:53:38 +0800 Subject: [PATCH 21/22] =?UTF-8?q?=F0=9F=93=9D=20Update=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2574dbd..878b1ed 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,20 @@ Before hosting this bot directly from this repo, There are few steps need to do, ```plain ArisaMatsuda/ -├── src -│   └── main.rs +. ├── Cargo.toml ├── LICENSE ├── README.md -└── README_zh-TW.md +├── rustfmt.toml +└── src + ├── main.rs + └── commands + ├── author.rs + ├── embed.rs + ├── instagram.rs + ├── mod.rs + ├── threads.rs + └── twitter.rs ``` ### Secrets From 6f4af64b2c69342000fbcc976ef193e2d2ab6cb1 Mon Sep 17 00:00:00 2001 From: LTurret Date: Mon, 25 May 2026 13:51:14 +0800 Subject: [PATCH 22/22] =?UTF-8?q?=F0=9F=90=9B=20Regex=20charset=20patch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/threads.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/threads.rs b/src/commands/threads.rs index 6c33874..791a5f2 100644 --- a/src/commands/threads.rs +++ b/src/commands/threads.rs @@ -114,7 +114,7 @@ impl ContentBuilder for ThreadBuilder { ) -> CreateMessage { let clean_url = format!( "https://www.threads.com/{}", - Regex::new(r"(?@.+/post/[\w]+)/?") + Regex::new(r"(?@.+/post/[a-zA-Z0-9._-]+)/?") .expect("Expected a valid regex pattern") .captures(endpoint) .expect("Expected a valid haystack")