cio_api/
applicants.rs

1use std::env;
2use std::fs;
3use std::io::{stderr, stdout, Write};
4use std::process::Command;
5
6use chrono::offset::Utc;
7use chrono::DateTime;
8use chrono_humanize::HumanTime;
9use google_drive::GoogleDrive;
10use html2text::from_read;
11use pandoc::OutputKind;
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16// The line breaks that get parsed are weird thats why we have the random asterisks here.
17static QUESTION_TECHNICALLY_CHALLENGING: &str = r"W(?s:.*)at work(?s:.*)ave you found mos(?s:.*)challenging(?s:.*)caree(?s:.*)wh(?s:.*)\?";
18static QUESTION_WORK_PROUD_OF: &str = r"W(?s:.*)at work(?s:.*)ave you done that you(?s:.*)particularl(?s:.*)proud o(?s:.*)and why\?";
19static QUESTION_HAPPIEST_CAREER: &str = r"W(?s:.*)en have you been happiest in your professiona(?s:.*)caree(?s:.*)and why\?";
20static QUESTION_UNHAPPIEST_CAREER: &str = r"W(?s:.*)en have you been unhappiest in your professiona(?s:.*)caree(?s:.*)and why\?";
21static QUESTION_VALUE_REFLECTED: &str = r"F(?s:.*)r one of Oxide(?s:.*)s values(?s:.*)describe an example of ho(?s:.*)it wa(?s:.*)reflected(?s:.*)particula(?s:.*)body(?s:.*)you(?s:.*)work\.";
22static QUESTION_VALUE_VIOLATED: &str = r"F(?s:.*)r one of Oxide(?s:.*)s values(?s:.*)describe an example of ho(?s:.*)it wa(?s:.*)violated(?s:.*)you(?s:.*)organization o(?s:.*)work\.";
23static QUESTION_VALUES_IN_TENSION: &str = r"F(?s:.*)r a pair of Oxide(?s:.*)s values(?s:.*)describe a time in whic(?s:.*)the tw(?s:.*)values(?s:.*)tensio(?s:.*)for(?s:.*)your(?s:.*)and how yo(?s:.*)resolved it\.";
24static QUESTION_WHY_OXIDE: &str = r"W(?s:.*)y do you want to work for Oxide\?";
25
26/// The data type for a Google Sheet Applicant Columns, we use this when updating the
27/// applications spreadsheet to mark that we have emailed someone.
28#[derive(Debug, Default, Deserialize, Serialize)]
29pub struct ApplicantSheetColumns {
30    pub timestamp: usize,
31    pub name: usize,
32    pub email: usize,
33    pub location: usize,
34    pub phone: usize,
35    pub github: usize,
36    pub portfolio: usize,
37    pub website: usize,
38    pub linkedin: usize,
39    pub resume: usize,
40    pub materials: usize,
41    pub status: usize,
42    pub received_application: usize,
43    pub value_reflected: usize,
44    pub value_violated: usize,
45    pub value_in_tension_1: usize,
46    pub value_in_tension_2: usize,
47}
48
49/// The data type for an applicant.
50#[derive(Debug, Clone)]
51pub struct Applicant {
52    pub submitted_time: DateTime<Utc>,
53    pub name: String,
54    pub email: String,
55    pub location: String,
56    pub phone: String,
57    pub country_code: String,
58    pub github: String,
59    pub gitlab: String,
60    pub portfolio: String,
61    pub website: String,
62    pub linkedin: String,
63    pub resume: String,
64    pub materials: String,
65    pub status: String,
66    pub received_application: bool,
67    pub role: String,
68    pub sheet_id: String,
69    pub value_reflected: String,
70    pub value_violated: String,
71    pub values_in_tension: Vec<String>,
72}
73
74impl Applicant {
75    pub async fn to_airtable(
76        &self,
77        drive_client: &GoogleDrive,
78    ) -> ApplicantFields {
79        let mut status = "Needs to be triaged";
80
81        if self.status.to_lowercase().contains("next steps") {
82            status = "Next steps";
83        } else if self.status.to_lowercase().contains("deferred") {
84            status = "Deferred";
85        } else if self.status.to_lowercase().contains("declined") {
86            status = "Declined";
87        } else if self.status.to_lowercase().contains("hired") {
88            status = "Hired";
89        }
90
91        let mut location = None;
92        if !self.location.is_empty() {
93            location = Some(self.location.to_string());
94        }
95
96        let mut github = None;
97        if !self.github.is_empty() {
98            github = Some(
99                "https://github.com/".to_owned()
100                    + &self.github.replace("@", ""),
101            );
102        }
103
104        let mut linkedin = None;
105        if !self.linkedin.is_empty() {
106            linkedin = Some(self.linkedin.to_string());
107        }
108
109        let mut portfolio = None;
110        if !self.portfolio.is_empty() {
111            portfolio = Some(self.portfolio.to_string());
112        }
113
114        let mut website = None;
115        if !self.website.is_empty() {
116            website = Some(self.website.to_string());
117        }
118
119        let mut value_reflected = None;
120        if !self.value_reflected.is_empty() {
121            value_reflected = Some(self.value_reflected.to_string());
122        }
123
124        let mut value_violated = None;
125        if !self.value_violated.is_empty() {
126            value_violated = Some(self.value_violated.to_string());
127        }
128
129        let mut values_in_tension = None;
130        if !self.values_in_tension.is_empty() {
131            values_in_tension = Some(self.values_in_tension.clone());
132        }
133
134        // Read the file contents.
135        let rc = get_file_contents(drive_client, self.resume.to_string()).await;
136        let mc =
137            get_file_contents(drive_client, self.materials.to_string()).await;
138
139        let mut resume_contents = None;
140        if !rc.is_empty() {
141            resume_contents = Some(rc);
142        }
143
144        let mut materials_contents = None;
145        if !mc.is_empty() {
146            materials_contents = Some(mc);
147        }
148
149        let mut applicant = ApplicantFields {
150            name: self.name.to_string(),
151            position: self.role.to_string(),
152            status: status.to_string(),
153            timestamp: self.submitted_time,
154            email: self.email.to_string(),
155            phone: self.phone.to_string(),
156            location,
157            github,
158            linkedin,
159            portfolio,
160            website,
161            resume: self.resume.to_string(),
162            materials: self.materials.to_string(),
163            value_reflected,
164            value_violated,
165            values_in_tension,
166            resume_contents,
167            materials_contents,
168            work_samples: None,
169            writing_samples: None,
170            analysis_samples: None,
171            presentation_samples: None,
172            exploratory_samples: None,
173            question_technically_challenging: None,
174            question_proud_of: None,
175            question_happiest: None,
176            question_unhappiest: None,
177            question_value_reflected: None,
178            question_value_violated: None,
179            question_values_in_tension: None,
180            question_why_oxide: None,
181        };
182
183        // Parse the materials.
184        applicant.parse_materials();
185
186        applicant
187    }
188
189    pub fn human_duration(&self) -> HumanTime {
190        let mut dur = self.submitted_time - Utc::now();
191        if dur.num_seconds() > 0 {
192            dur = -dur;
193        }
194
195        HumanTime::from(dur)
196    }
197
198    pub fn as_slack_msg(&self) -> Value {
199        let mut color = "#805AD5";
200        match self.role.as_str() {
201            "Product Engineering and Design" => color = "#48D597",
202            "Technical Program Management" => color = "#667EEA",
203            _ => (),
204        }
205
206        let time = self.human_duration();
207
208        let mut status_msg = format!("<https://docs.google.com/spreadsheets/d/{}|{}> Applicant | applied {}", self.sheet_id, self.role, time);
209        if !self.status.is_empty() {
210            status_msg += &format!(" | status: *{}*", self.status);
211        }
212
213        let mut values_msg = "".to_string();
214        if !self.value_reflected.is_empty() {
215            values_msg +=
216                &format!("values reflected: *{}*", self.value_reflected);
217        }
218        if !self.value_violated.is_empty() {
219            values_msg += &format!(" | violated: *{}*", self.value_violated);
220        }
221        for (k, tension) in self.values_in_tension.iter().enumerate() {
222            if k == 0 {
223                values_msg += &format!(" | in tension: *{}*", tension);
224            } else {
225                values_msg += &format!(" *& {}*", tension);
226            }
227        }
228        if values_msg.is_empty() {
229            values_msg = "values not yet populated".to_string();
230        }
231
232        let mut intro_msg =
233            format!("*{}*  <mailto:{}|{}>", self.name, self.email, self.email,);
234        if !self.location.is_empty() {
235            intro_msg += &format!("  {}", self.location);
236        }
237
238        let mut info_msg = format!(
239            "<{}|resume> | <{}|materials>",
240            self.resume, self.materials,
241        );
242        if !self.phone.is_empty() {
243            info_msg += &format!(" | <tel:{}|{}>", self.phone, self.phone);
244        }
245        if !self.github.is_empty() {
246            info_msg += &format!(
247                " | <https://github.com/{}|github:{}>",
248                self.github.trim_start_matches('@'),
249                self.github,
250            );
251        }
252        if !self.gitlab.is_empty() {
253            info_msg += &format!(
254                " | <https://gitlab.com/{}|gitlab:{}>",
255                self.gitlab.trim_start_matches('@'),
256                self.gitlab,
257            );
258        }
259        if !self.linkedin.is_empty() {
260            info_msg += &format!(" | <{}|linkedin>", self.linkedin,);
261        }
262        if !self.portfolio.is_empty() {
263            info_msg += &format!(" | <{}|portfolio>", self.portfolio,);
264        }
265        if !self.website.is_empty() {
266            info_msg += &format!(" | <{}|website>", self.website,);
267        }
268
269        json!({
270            "response_type": "in_channel",
271            "attachments": [
272                {
273                    "color": color,
274                    "blocks": [
275                        {
276                            "type": "section",
277                            "text": {
278                                "type": "mrkdwn",
279                                "text": intro_msg
280                            }
281                        },
282                        {
283                            "type": "context",
284                            "elements": [
285                                {
286                                    "type": "mrkdwn",
287                                    "text": info_msg
288                                }
289                            ]
290                        },
291                        {
292                            "type": "context",
293                            "elements": [
294                                {
295                                    "type": "mrkdwn",
296                                    "text": values_msg
297                                }
298                            ]
299                        },
300                        {
301                            "type": "context",
302                            "elements": [
303                                {
304                                    "type": "mrkdwn",
305                                    "text": status_msg
306                                }
307                            ]
308                        }
309                    ]
310                }
311            ]
312        })
313    }
314
315    pub fn as_company_notification_email(&self) -> String {
316        let time = self.human_duration();
317
318        let mut msg = format!(
319            "## Applicant Information for {}
320
321Submitted {}
322Name: {}
323Email: {}",
324            self.role, time, self.name, self.email
325        );
326
327        if !self.location.is_empty() {
328            msg += &format!("\nLocation: {}", self.location);
329        }
330        if !self.phone.is_empty() {
331            msg += &format!("\nPhone: {}", self.phone);
332        }
333
334        if !self.github.is_empty() {
335            msg += &format!(
336                "\nGitHub: {} (https://github.com/{})",
337                self.github,
338                self.github.trim_start_matches('@')
339            );
340        }
341        if !self.gitlab.is_empty() {
342            msg += &format!(
343                "\nGitLab: {} (https://gitlab.com/{})",
344                self.gitlab,
345                self.gitlab.trim_start_matches('@')
346            );
347        }
348        if !self.linkedin.is_empty() {
349            msg += &format!("\nLinkedIn: {}", self.linkedin);
350        }
351        if !self.portfolio.is_empty() {
352            msg += &format!("\nPortfolio: {}", self.portfolio);
353        }
354        if !self.website.is_empty() {
355            msg += &format!("\nWebsite: {}", self.website);
356        }
357
358        msg+=&format!("\nResume: {}
359Oxide Candidate Materials: {}
360
361## Reminder
362
363To view the all the candidates refer to the following Google spreadsheets:
364
365- Engineering Applications: https://applications-engineering.corp.oxide.computer
366- Product Engineering and Design Applications: https://applications-product.corp.oxide.computer
367- Technical Program Manager Applications: https://applications-tpm.corp.oxide.computer
368",
369                        self.resume,
370                        self.materials,
371                    );
372
373        msg
374    }
375}
376
377/// The Airtable fields type for Applicants.
378#[derive(Debug, Clone, Deserialize, Serialize)]
379pub struct ApplicantFields {
380    #[serde(rename = "Name")]
381    pub name: String,
382    #[serde(rename = "Position")]
383    pub position: String,
384    #[serde(rename = "Status")]
385    pub status: String,
386    #[serde(rename = "Timestamp")]
387    pub timestamp: DateTime<Utc>,
388    #[serde(rename = "Email Address")]
389    pub email: String,
390    #[serde(rename = "Phone Number")]
391    pub phone: String,
392    #[serde(rename = "Location")]
393    pub location: Option<String>,
394    #[serde(rename = "GitHub")]
395    pub github: Option<String>,
396    #[serde(rename = "LinkedIn")]
397    pub linkedin: Option<String>,
398    #[serde(rename = "Portfolio")]
399    pub portfolio: Option<String>,
400    #[serde(rename = "Website")]
401    pub website: Option<String>,
402    #[serde(rename = "Resume")]
403    pub resume: String,
404    #[serde(rename = "Oxide Materials")]
405    pub materials: String,
406    #[serde(rename = "Value Reflected")]
407    pub value_reflected: Option<String>,
408    #[serde(rename = "Value Violated")]
409    pub value_violated: Option<String>,
410    #[serde(rename = "Values in Tension")]
411    pub values_in_tension: Option<Vec<String>>,
412    #[serde(rename = "Resume Contents")]
413    pub resume_contents: Option<String>,
414    #[serde(rename = "Oxide Materials Contents")]
415    pub materials_contents: Option<String>,
416    #[serde(rename = "Work samples")]
417    pub work_samples: Option<String>,
418    #[serde(rename = "Writing samples")]
419    pub writing_samples: Option<String>,
420    #[serde(rename = "Analysis samples")]
421    pub analysis_samples: Option<String>,
422    #[serde(rename = "Presentation samples")]
423    pub presentation_samples: Option<String>,
424    #[serde(rename = "Exploratory samples")]
425    pub exploratory_samples: Option<String>,
426    #[serde(
427        rename = "What work have you found most technically challenging in your career and why?"
428    )]
429    pub question_technically_challenging: Option<String>,
430    #[serde(
431        rename = "What work have you done that you were particularly proud of and why?"
432    )]
433    pub question_proud_of: Option<String>,
434    #[serde(
435        rename = "When have you been happiest in your professional career and why?"
436    )]
437    pub question_happiest: Option<String>,
438    #[serde(
439        rename = "When have you been unhappiest in your professional career and why?"
440    )]
441    pub question_unhappiest: Option<String>,
442    #[serde(
443        rename = "For one of Oxide's values, describe an example of how it was reflected in a particular body of your work."
444    )]
445    pub question_value_reflected: Option<String>,
446    #[serde(
447        rename = "For one of Oxide's values, describe an example of how it was violated in your organization or work."
448    )]
449    pub question_value_violated: Option<String>,
450    #[serde(
451        rename = "For a pair of Oxide's values, describe a time in which the two values came into tension for you or your work, and how you resolved it."
452    )]
453    pub question_values_in_tension: Option<String>,
454    #[serde(rename = "Why do you want to work for Oxide?")]
455    pub question_why_oxide: Option<String>,
456}
457
458impl PartialEq for ApplicantFields {
459    fn eq(&self, other: &Self) -> bool {
460        self.name == other.name
461            && self.position == other.position
462            && self.status == other.status
463            && self.timestamp == other.timestamp
464            && self.email == other.email
465            && self.phone == other.phone
466            && self.location == other.location
467            && self.github == other.github
468            && self.linkedin == other.linkedin
469            && self.portfolio == other.portfolio
470            && self.website == other.website
471            && self.resume == other.resume
472            && self.materials == other.materials
473            && self.value_reflected == other.value_reflected
474            && self.value_violated == other.value_violated
475            && self.values_in_tension == other.values_in_tension
476            && self.resume_contents == other.resume_contents
477            && self.materials_contents == other.materials_contents
478            && self.work_samples == other.work_samples
479            && self.writing_samples == other.writing_samples
480            && self.analysis_samples == other.analysis_samples
481            && self.presentation_samples == other.presentation_samples
482            && self.exploratory_samples == other.exploratory_samples
483            && self.question_technically_challenging
484                == other.question_technically_challenging
485            && self.question_proud_of == other.question_proud_of
486            && self.question_happiest == other.question_happiest
487            && self.question_unhappiest == other.question_unhappiest
488            && self.question_value_reflected == other.question_value_reflected
489            && self.question_value_violated == other.question_value_violated
490            && self.question_values_in_tension
491                == other.question_values_in_tension
492            && self.question_why_oxide == other.question_why_oxide
493    }
494}
495
496impl ApplicantFields {
497    // TODO: probably a better way to do regexes here, but hey it works.
498    fn parse_materials(&mut self) {
499        let materials_contents;
500        match &self.materials_contents {
501            Some(m) => materials_contents = m,
502            None => return,
503        }
504
505        let mut work_samples = parse_question(
506            r"Work sample\(s\)",
507            "Writing samples",
508            materials_contents,
509        );
510        if work_samples == None {
511            work_samples = parse_question(
512                r"If(?s:.*)his work is entirely proprietary(?s:.*)please describe it as fully as y(?s:.*)can, providing necessary context\.",
513                "Writing samples",
514                materials_contents,
515            );
516            if work_samples == None {
517                // Try to parse work samples for TPM role.
518                work_samples = parse_question(
519                    r"What would you have done differently\?",
520                    "Exploratory samples",
521                    materials_contents,
522                );
523
524                if work_samples == None {
525                    work_samples = parse_question(
526                        r"Some questions(?s:.*)o have in mind as you describe them:",
527                        "Exploratory samples",
528                        materials_contents,
529                    );
530
531                    if work_samples == None {
532                        work_samples = parse_question(
533                            r"Work samples",
534                            "Exploratory samples",
535                            materials_contents,
536                        );
537                    }
538                }
539            }
540        }
541        self.work_samples = work_samples;
542
543        let mut writing_samples = parse_question(
544            r"Writing sample\(s\)",
545            "Analysis samples",
546            materials_contents,
547        );
548        if writing_samples == None {
549            writing_samples = parse_question(
550                r"Please submit at least one writing sample \(and no more tha(?s:.*)three\) that you feel represent(?s:.*)you(?s:.*)providin(?s:.*)links if(?s:.*)necessary\.",
551                "Analysis samples",
552                materials_contents,
553            );
554            if writing_samples == None {
555                writing_samples = parse_question(
556                    r"Writing samples",
557                    "Analysis samples",
558                    materials_contents,
559                );
560            }
561        }
562        self.writing_samples = writing_samples;
563
564        let mut analysis_samples = parse_question(
565            r"Analysis sample\(s\)$",
566            "Presentation samples",
567            materials_contents,
568        );
569        if analysis_samples == None {
570            analysis_samples = parse_question(
571                r"please recount a(?s:.*)incident(?s:.*)which you analyzed syste(?s:.*)misbehavior(?s:.*)including as much technical detail as you can recall\.",
572                "Presentation samples",
573                materials_contents,
574            );
575            if analysis_samples == None {
576                analysis_samples = parse_question(
577                    r"Analysis samples",
578                    "Presentation samples",
579                    materials_contents,
580                );
581            }
582        }
583        self.analysis_samples = analysis_samples;
584
585        let mut presentation_samples = parse_question(
586            r"Presentation sample\(s\)",
587            "Questionnaire",
588            materials_contents,
589        );
590        if presentation_samples == None {
591            presentation_samples = parse_question(
592                r"I(?s:.*)you don’t have a publicl(?s:.*)available presentation(?s:.*)pleas(?s:.*)describe a topic on which you have presented in th(?s:.*)past\.",
593                "Questionnaire",
594                materials_contents,
595            );
596            if presentation_samples == None {
597                presentation_samples = parse_question(
598                    r"Presentation samples",
599                    "Questionnaire",
600                    materials_contents,
601                );
602            }
603        }
604        self.presentation_samples = presentation_samples;
605
606        let mut exploratory_samples = parse_question(
607            r"Exploratory sample\(s\)",
608            "Questionnaire",
609            materials_contents,
610        );
611        if exploratory_samples == None {
612            exploratory_samples = parse_question(
613                r"What’s an example o(?s:.*)something that you needed to explore, reverse engineer, decipher or otherwise figure out a(?s:.*)part of a program or project and how did you do it\? Please provide as much detail as you ca(?s:.*)recall\.",
614                "Questionnaire",
615                materials_contents,
616            );
617            if exploratory_samples == None {
618                exploratory_samples = parse_question(
619                    r"Exploratory samples",
620                    "Questionnaire",
621                    materials_contents,
622                );
623            }
624        }
625        self.exploratory_samples = exploratory_samples;
626
627        let question_technically_challenging = parse_question(
628            QUESTION_TECHNICALLY_CHALLENGING,
629            QUESTION_WORK_PROUD_OF,
630            materials_contents,
631        );
632        self.question_technically_challenging =
633            question_technically_challenging;
634
635        let question_proud_of = parse_question(
636            QUESTION_WORK_PROUD_OF,
637            QUESTION_HAPPIEST_CAREER,
638            materials_contents,
639        );
640        self.question_proud_of = question_proud_of;
641
642        let question_happiest = parse_question(
643            QUESTION_HAPPIEST_CAREER,
644            QUESTION_UNHAPPIEST_CAREER,
645            materials_contents,
646        );
647        self.question_happiest = question_happiest;
648
649        let question_unhappiest = parse_question(
650            QUESTION_UNHAPPIEST_CAREER,
651            QUESTION_VALUE_REFLECTED,
652            materials_contents,
653        );
654        self.question_unhappiest = question_unhappiest;
655
656        let question_value_reflected = parse_question(
657            QUESTION_VALUE_REFLECTED,
658            QUESTION_VALUE_VIOLATED,
659            materials_contents,
660        );
661        self.question_value_reflected = question_value_reflected;
662
663        let question_value_violated = parse_question(
664            QUESTION_VALUE_VIOLATED,
665            QUESTION_VALUES_IN_TENSION,
666            materials_contents,
667        );
668        self.question_value_violated = question_value_violated;
669
670        let question_values_in_tension = parse_question(
671            QUESTION_VALUES_IN_TENSION,
672            QUESTION_WHY_OXIDE,
673            materials_contents,
674        );
675        self.question_values_in_tension = question_values_in_tension;
676
677        let question_why_oxide =
678            parse_question(QUESTION_WHY_OXIDE, "", materials_contents);
679        self.question_why_oxide = question_why_oxide;
680    }
681}
682
683fn parse_question(
684    q1: &str,
685    q2: &str,
686    materials_contents: &str,
687) -> Option<String> {
688    let re = Regex::new(&(q1.to_owned() + r"(?s)(.*)" + q2)).unwrap();
689    let result: Option<String> = if let Some(q) =
690        re.captures(materials_contents)
691    {
692        let val = q.get(1).unwrap();
693        let s = val
694            .as_str()
695            .replace("________________", "")
696            .replace("Oxide Candidate Materials: Technical Program Manager", "")
697            .replace("Oxide Candidate Materials", "")
698            .replace("Work sample(s)", "")
699            .trim_start_matches(':')
700            .trim()
701            .to_string();
702
703        if s.is_empty() {
704            return None;
705        }
706
707        Some(s)
708    } else {
709        None
710    };
711
712    result
713}
714
715/// Get the contexts of a file in Google Drive by it's URL as a text string.
716async fn get_file_contents(drive_client: &GoogleDrive, url: String) -> String {
717    let id = url.replace("https://drive.google.com/open?id=", "");
718
719    // Get information about the file.
720    let drive_file = drive_client.get_file_by_id(&id).await.unwrap();
721    let mime_type = drive_file.mime_type.unwrap();
722    let name = drive_file.name.unwrap();
723
724    let mut path = env::temp_dir();
725    let mut output = env::temp_dir();
726
727    let mut result: String = Default::default();
728
729    if mime_type == "application/pdf" {
730        // Get the PDF contents from Drive.
731        let contents = drive_client.download_file_by_id(&id).await.unwrap();
732
733        path.push(format!("{}.pdf", id));
734
735        let mut file = fs::File::create(path.clone()).unwrap();
736        file.write_all(&contents).unwrap();
737
738        output.push(format!("{}.txt", id));
739
740        // Extract the text from the PDF
741        let cmd_output = Command::new("pdftotext")
742            .args(&[
743                "-enc",
744                "UTF-8",
745                path.to_str().unwrap(),
746                output.to_str().unwrap(),
747            ])
748            .output()
749            .unwrap();
750
751        result = match fs::read_to_string(output.clone()) {
752            Ok(r) => r,
753            Err(e) => {
754                println!(
755                    "running pdf2text failed: {} | name: {}, path: {}",
756                    e,
757                    name,
758                    path.to_str().unwrap()
759                );
760                stdout().write_all(&cmd_output.stdout).unwrap();
761                stderr().write_all(&cmd_output.stderr).unwrap();
762
763                "".to_string()
764            }
765        };
766    } else if mime_type == "text/html" {
767        let contents = drive_client.download_file_by_id(&id).await.unwrap();
768
769        // Wrap lines at 80 characters.
770        result = from_read(&contents[..], 80);
771    } else if mime_type == "application/vnd.google-apps.document" {
772        result = drive_client.get_file_contents_by_id(&id).await.unwrap();
773    } else if name.ends_with(".doc")
774        || name.ends_with(".pptx")
775        || name.ends_with(".jpg")
776        || name.ends_with(".zip")
777    // TODO: handle these formats
778    {
779        println!(
780            "unsupported doc format -- mime type: {}, name: {}, path: {}",
781            mime_type,
782            name,
783            path.to_str().unwrap()
784        );
785    } else {
786        let contents = drive_client.download_file_by_id(&id).await.unwrap();
787        path.push(name.to_string());
788
789        let mut file = fs::File::create(path.clone()).unwrap();
790        file.write_all(&contents).unwrap();
791
792        output.push(format!("{}.txt", id));
793
794        let mut pandoc = pandoc::new();
795        pandoc.add_input(&path);
796        pandoc.set_output(OutputKind::File(output.clone()));
797        pandoc.execute().unwrap();
798
799        result = fs::read_to_string(output.clone()).unwrap();
800    }
801
802    // Delete the temporary file, if it exists.
803    for p in vec![path, output] {
804        if p.exists() && !p.is_dir() {
805            fs::remove_file(p).unwrap();
806        }
807    }
808
809    result.trim().to_string()
810}