silly goober bot
1// the logic here is pretty much ripped from https://github.com/uncenter/discord-forum-bot/blob/main/src/modules/expandGitHubLinks.ts
2// with some modifications so I can make it work on diffrent git hosts
3
4use color_eyre::eyre::{eyre, Result};
5use poise::serenity_prelude::{Context, FullEvent};
6use regex::Regex;
7use reqwest::Client;
8
9use crate::types::Data;
10
11pub async fn handle(ctx: &Context, event: &FullEvent, data: &Data) -> Result<()> {
12 if let FullEvent::Message { new_message } = event {
13 let code_blocks = extract_code_blocks(new_message.content.clone(), &data.client).await?;
14
15 if !code_blocks.is_empty() {
16 new_message
17 .channel_id
18 .say(ctx, code_blocks.join("\n"))
19 .await?;
20 }
21 }
22
23 Ok(())
24}
25
26async fn extract_code_blocks(msg: String, client: &Client) -> Result<Vec<String>> {
27 let re = Regex::new(
28 r"https?://(?P<host>(git.*|codeberg\.org))/(?P<repo>[\w-]+/[\w.-]+)/(blob|(src/(commit|branch)))?/(?P<reference>\S+?)/(?P<file>\S+)#L(?P<start>\d+)(?:[~-]L?(?P<end>\d+)?)?",
29 )?;
30
31 let mut blocks: Vec<String> = Vec::new();
32
33 for caps in re.captures_iter(&msg) {
34 let (host, repo, reference, file, start, end) = extract_url_components(&caps)?;
35
36 let raw_url = construct_raw_url(host, repo, reference, file);
37
38 if let Ok(code_block) = fetch_code_block(client, &raw_url, start, end, file).await {
39 blocks.push(code_block);
40 }
41 }
42
43 Ok(blocks)
44}
45
46fn extract_url_components<'a>(
47 caps: &'a regex::Captures<'a>,
48) -> Result<(&'a str, &'a str, &'a str, &'a str, usize, usize)> {
49 let host = &caps["host"];
50 let repo = &caps["repo"];
51 let reference = &caps["reference"];
52 let file = &caps["file"];
53 let start = caps["start"].parse::<usize>()?;
54 let end = caps
55 .name("end")
56 .map_or(Ok(start), |end| end.as_str().parse::<usize>())?;
57
58 Ok((host, repo, reference, file, start, end))
59}
60
61fn construct_raw_url(host: &str, repo: &str, reference: &str, file: &str) -> String {
62 if host == "github.com" {
63 format!("https://raw.githubusercontent.com/{repo}/{reference}/{file}")
64 } else {
65 let refer = if reference.len() == 40 {
66 format!("commit/{reference}")
67 } else {
68 format!("branch/{reference}")
69 };
70 format!("https://{host}/{repo}/raw/{refer}/{file}")
71 }
72}
73
74async fn fetch_code_block(
75 client: &Client,
76 raw_url: &str,
77 start: usize,
78 end: usize,
79 file: &str,
80) -> Result<String> {
81 let response = client.get(raw_url).send().await?;
82 if !response.status().is_success() {
83 return Err(eyre!("Failed to fetch content from {}", raw_url));
84 }
85
86 let text = response.text().await?;
87 let content = text
88 .lines()
89 .skip(start - 1)
90 .take(end - start + 1)
91 .collect::<Vec<&str>>()
92 .join("\n");
93
94 let language = file
95 .split('.')
96 .next_back()
97 .map_or("", remove_query_string)
98 .to_lowercase();
99
100 Ok(format_code_block(&language, &content))
101}
102
103fn format_code_block(language: &str, content: &str) -> String {
104 if content.len() > 1950 {
105 let truncated_content = content.lines().take(1950).collect::<Vec<&str>>().join("\n");
106 format!("```{language}\n{truncated_content}\n```\n... (lines not displayed)")
107 } else {
108 format!("```{language}\n{content}\n```")
109 }
110}
111
112fn remove_query_string(input: &str) -> &str {
113 input.split('?').next().unwrap_or(input)
114}