dev_scope/shared/
report.rs

1use super::capture::{CaptureOpts, OutputCapture};
2use super::config_load::FoundConfig;
3use super::models::prelude::ReportUploadLocationDestination;
4use super::prelude::OutputDestination;
5use anyhow::{anyhow, Result};
6use minijinja::{context, Environment};
7use reqwest::header::{ACCEPT, AUTHORIZATION, USER_AGENT};
8use std::fs::File;
9use std::io::Write;
10use tracing::{debug, info, warn};
11
12pub struct ReportBuilder<'a> {
13    message: String,
14    command_results: String,
15    config: &'a FoundConfig,
16}
17
18impl<'a> ReportBuilder<'a> {
19    pub async fn new(capture: &OutputCapture, config: &'a FoundConfig) -> Result<Self> {
20        let message = Self::make_default_message(&capture.command, config)?;
21
22        let mut this = Self {
23            message,
24            command_results: String::new(),
25            config,
26        };
27
28        this.add_capture(capture)?;
29
30        for command in config.get_report_definition().additional_data.values() {
31            let args: Vec<String> = command.split(' ').map(|x| x.to_string()).collect();
32            let capture = OutputCapture::capture_output(CaptureOpts {
33                working_dir: &config.working_dir,
34                args: &args,
35                output_dest: OutputDestination::Null,
36                path: &config.bin_path,
37                env_vars: Default::default(),
38            })
39            .await?;
40            this.add_capture(&capture)?;
41        }
42
43        Ok(this)
44    }
45
46    fn add_capture(&mut self, capture: &OutputCapture) -> Result<()> {
47        self.command_results.push('\n');
48        self.command_results
49            .push_str(&capture.create_report_text()?);
50
51        Ok(())
52    }
53
54    pub fn write_local_report(&self) -> Result<()> {
55        let report = self.make_report_test();
56
57        let base_report_loc = write_to_report_file("base", &report)?;
58        info!(target: "always", "The basic report was created at {}", base_report_loc);
59
60        Ok(())
61    }
62
63    fn make_default_message(command: &str, config: &FoundConfig) -> Result<String> {
64        let mut env = Environment::new();
65        let report_def = config.get_report_definition();
66        env.add_template("tmpl", &report_def.template)?;
67        let template = env.get_template("tmpl")?;
68        let template = template.render(context! { command => command })?;
69
70        Ok(template)
71    }
72
73    fn make_report_test(&self) -> String {
74        format!(
75            "{}\n\n## Captured Data\n\n{}",
76            self.message, self.command_results
77        )
78    }
79
80    pub async fn distribute_report(&self) -> Result<()> {
81        let report = self.make_report_test();
82
83        for dest in self.config.report_upload.values() {
84            if let Err(e) = &dest.destination.upload(&report).await {
85                warn!(target: "user", "Unable to upload to {}: {}", dest.metadata.name(), e);
86            }
87        }
88
89        Ok(())
90    }
91}
92
93impl ReportUploadLocationDestination {
94    async fn upload(&self, report: &str) -> Result<()> {
95        match self {
96            ReportUploadLocationDestination::RustyPaste { url } => {
97                ReportUploadLocationDestination::upload_to_rusty_paste(url, report).await
98            }
99            ReportUploadLocationDestination::GithubIssue { owner, repo, tags } => {
100                ReportUploadLocationDestination::upload_to_github_issue(
101                    owner,
102                    repo,
103                    tags.clone(),
104                    report,
105                )
106                .await
107            }
108        }
109    }
110
111    async fn upload_to_github_issue(
112        owner: &str,
113        repo: &str,
114        tags: Vec<String>,
115        report: &str,
116    ) -> Result<()> {
117        let gh_auth = match std::env::var("GH_TOKEN") {
118            Ok(v) => v,
119            Err(_) => {
120                return Err(anyhow!(
121                    "GH_TOKEN env var was not set with token to access GitHub"
122                ))
123            }
124        };
125
126        let title = match report.find('\n') {
127            Some(value) => report[0..value].to_string(),
128            None => "Scope bug report".to_string(),
129        };
130
131        let body = json::object! {
132            title: title,
133            body: report,
134            labels: tags
135        };
136
137        let client = reqwest::Client::new();
138        let res = client
139            .post(format!(
140                "https://api.github.com/repos/{}/{}/issues",
141                owner, repo
142            ))
143            .header(ACCEPT, "application/vnd.github+json")
144            .header(AUTHORIZATION, format!("Bearer {}", gh_auth))
145            .header(USER_AGENT, "scope")
146            .header("X-GitHub-Api-Version", "2022-11-28")
147            .body(body.dump())
148            .send()
149            .await;
150
151        match res {
152            Ok(res) => {
153                debug!("API Response was {:?}", res);
154                let status = res.status();
155                match res.text().await {
156                    Err(e) => {
157                        warn!(target: "user", "Unable to read Github response: {:?}", e)
158                    }
159                    Ok(body) => {
160                        let body = body.trim();
161                        if status.is_success() {
162                            match json::parse(body) {
163                                Ok(json_body) => {
164                                    info!(target: "always", "Report was uploaded to {}.", json_body["html_url"])
165                                }
166                                Err(e) => {
167                                    warn!(server = "github", "GitHub response {}", body);
168                                    warn!(server = "github", "GitHub parse error {:?}", e);
169                                    warn!(target: "always", server="github", "GitHub responded with weird response, please check the logs.");
170                                }
171                            }
172                        } else {
173                            info!(target: "always", server="github", "Report upload failed for {}.", body)
174                        }
175                    }
176                }
177            }
178            Err(e) => {
179                warn!(target: "always", "Unable to upload report to server because {}", e)
180            }
181        }
182
183        Ok(())
184    }
185
186    async fn upload_to_rusty_paste(url: &str, report: &str) -> Result<()> {
187        let client = reqwest::Client::new();
188        let some_file = reqwest::multipart::Part::stream(report.to_string())
189            .file_name("file")
190            .mime_str("text/plain")?;
191
192        let form = reqwest::multipart::Form::new().part("file", some_file);
193
194        let res = client.post(url).multipart(form).send().await;
195
196        match res {
197            Ok(res) => {
198                debug!(server = "RustyPaste", "API Response was {:?}", res);
199                let status = res.status();
200                match res.text().await {
201                    Err(e) => {
202                        warn!(target: "user",server="RustyPaste",  "Unable to fetch body from Server: {:?}", e)
203                    }
204                    Ok(body) => {
205                        let body = body.trim();
206                        if !status.is_success() {
207                            info!(target: "always", server="RustyPaste", "Report was uploaded to {}.", body)
208                        } else {
209                            info!(target: "always", server="RustyPaste", "Report upload failed for {}.", body)
210                        }
211                    }
212                }
213            }
214            Err(e) => {
215                warn!(target: "always", server="RustyPaste", "Unable to upload report to server because {}", e)
216            }
217        }
218        Ok(())
219    }
220}
221
222pub fn write_to_report_file(prefix: &str, text: &str) -> Result<String> {
223    let id = nanoid::nanoid!(10, &nanoid::alphabet::SAFE);
224
225    let file_path = format!("/tmp/scope/scope-{}-{}.md", prefix, id);
226    let mut file = File::create(&file_path)?;
227    file.write_all(text.as_bytes())?;
228
229    Ok(file_path)
230}