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}