use crate::connection::{send_http_request, HttpMethod, SYNC_ATTEMPT};
use crate::{
course, Assignment, AssignmentInfo, CanvasCredentials, Course, CourseInfo, Student,
StudentInfo, Submission,
};
use course::parse_course_name;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Select;
use reqwest::blocking::multipart::{Form, Part};
use reqwest::blocking::Client;
use serde_json::Value;
use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
use std::thread::sleep;
use serde_json::json;
pub enum CanvasResultCourses {
Ok(Vec<Course>), ErrConnection(String), ErrCredentials(String), }
pub enum CanvasResultSingleCourse {
Ok(Course), ErrConnection(String), ErrCredentials(String), }
pub struct Canvas {
}
impl Canvas {
pub fn fetch_courses_with_credentials(info: &CanvasCredentials) -> CanvasResultCourses {
let canvas_info_arc = Arc::new((*info).clone());
let url = format!("{}/courses", info.url_canvas);
let mut all_courses = Vec::new();
let mut page = 1;
let client = &Client::new();
loop {
let params = vec![
(
"enrollment_role".to_string(),
"TeacherEnrollment".to_string(),
),
("page".to_string(), page.to_string()),
("per_page".to_string(), "100".to_string()),
];
match send_http_request(&client, HttpMethod::Get, &url, &info, params) {
Ok(response) => {
if response.status().is_success() {
match response.text() {
Ok(text) => {
match serde_json::from_str::<Vec<serde_json::Value>>(&text) {
Ok(courses) => {
if courses.is_empty() {
break; }
all_courses.extend(courses.iter().filter_map(|course| {
Canvas::convert_json_to_course(&canvas_info_arc, course)
}));
page += 1; }
Err(e) => {
return CanvasResultCourses::ErrCredentials(format!(
"Failed to parse courses JSON with error: {}",
e
));
}
}
}
Err(e) => {
return CanvasResultCourses::ErrCredentials(format!(
"Failed to read response text with error: {}",
e
));
}
}
} else {
return CanvasResultCourses::ErrCredentials(format!(
"Failed to fetch courses with status: {}",
response.status()
));
}
}
Err(e) => {
return CanvasResultCourses::ErrConnection(format!(
"Failed to fetch courses with error: {}",
e
));
}
}
}
CanvasResultCourses::Ok(all_courses)
}
pub fn fetch_single_course_with_credentials(
info: &CanvasCredentials,
course_id: u64,
) -> CanvasResultSingleCourse {
let canvas_info_arc = Arc::new((*info).clone());
let url = format!("{}/courses/{}", info.url_canvas, course_id);
match send_http_request(
&Client::new(),
HttpMethod::Get,
&url,
info,
Vec::new(), ) {
Ok(response) => {
if response.status().is_success() {
let course: serde_json::Value = response.json().unwrap();
if let Some(course) = Canvas::convert_json_to_course(&canvas_info_arc, &course)
{
return CanvasResultSingleCourse::Ok(course);
} else {
return CanvasResultSingleCourse::ErrConnection(
"Failed to parse course data".to_string(),
);
}
} else {
CanvasResultSingleCourse::ErrConnection(format!(
"Failed to fetch course: HTTP Status {}",
response.status()
))
}
}
Err(e) => {
CanvasResultSingleCourse::ErrConnection(format!("HTTP request failed: {}", e))
}
}
}
fn convert_json_to_course(
canvas_info: &Arc<CanvasCredentials>,
course: &serde_json::Value,
) -> Option<Course> {
let id = course["id"].as_u64()?;
let name = course["name"].as_str().map(String::from)?;
let course_code = course["course_code"].as_str().map(String::from)?;
Some(Course {
info: Arc::new(CourseInfo {
id,
name: name.clone(),
course_code: course_code.clone(),
canvas_info: Arc::clone(canvas_info),
abbreviated_name: parse_course_name(name.as_str(), course_code.as_str()), }),
})
}
pub fn choose_course() -> Option<Course> {
let mut menu_str = Vec::new();
let mut menu_course = Vec::new();
let credentials = CanvasCredentials::credentials();
println!("Fetching courses...");
match Canvas::fetch_courses_with_credentials(&credentials) {
CanvasResultCourses::Ok(courses) => {
for course in courses {
if let Some(course_details_name) = parse_course_name(
course.info.name.as_str(),
course.info.course_code.as_str(),
) {
menu_str.push(course_details_name.abbreviated_name);
menu_course.push(course);
}
}
}
CanvasResultCourses::ErrConnection(msg) => {
eprintln!("Connection error: {}", msg);
std::process::exit(1);
}
CanvasResultCourses::ErrCredentials(msg) => {
eprintln!("Credential error: {}", msg);
std::process::exit(1);
}
}
menu_str.push("EXIT".to_string());
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose a course")
.items(&menu_str)
.default(0)
.interact()
.unwrap();
if selection == menu_str.len() - 1 {
return None;
}
Some(menu_course[selection].clone())
}
}
pub fn get_current_year_and_semester() -> (String, String) {
use chrono::{Datelike, Utc};
let current_date = Utc::now();
let year = current_date.year().to_string();
let semester =
if current_date.month() < 7 || (current_date.month() == 7 && current_date.day() <= 15) {
"1".to_string()
} else {
"2".to_string()
};
(year, semester)
}
fn add_comment(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: &str,
user_id: &str,
comment_text: &str,
file_ids: Option<Vec<i64>>,
) -> Result<(), Box<dyn Error>> {
let url = format!(
"{}/courses/{}/assignments/{}/submissions/{}",
canvas_info.url_canvas, course_id, assignment_id, user_id
);
let mut body = serde_json::json!({
"comment": {
"text_comment": comment_text
}
});
if let Some(file_ids) = file_ids {
body["comment"]["file_ids"] = serde_json::json!(file_ids);
}
send_http_request(client, HttpMethod::Put(body), &url, &canvas_info, vec![])
.map_err(|e| format!("Failed to add comment: {}", e))?;
Ok(())
}
pub fn request_upload_token(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: &str,
user_id: &str,
file_name: &str,
file_size: u64,
) -> Result<(String, HashMap<String, String>), Box<dyn Error>> {
let url = format!(
"{}/courses/{}/assignments/{}/submissions/{}/comments/files",
canvas_info.url_canvas, course_id, assignment_id, user_id
);
let body = serde_json::json!({
"name": file_name,
"size": file_size
});
match send_http_request(
client,
HttpMethod::Post(body), &url,
&canvas_info,
vec![], ) {
Ok(response) => {
if response.status().is_success() {
let json_response: serde_json::Value = response.json()?;
let upload_url = json_response["upload_url"]
.as_str()
.ok_or("Missing upload_url")?
.to_string();
let upload_params = json_response["upload_params"]
.as_object()
.ok_or("Missing upload_params")?;
let mut params = HashMap::new();
for (key, value) in upload_params {
let value_str = value.as_str().ok_or("Invalid param value")?;
params.insert(key.clone(), value_str.to_string());
}
Ok((upload_url, params))
} else {
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to request upload token with status: {}",
response.status()
),
)))
}
}
Err(e) => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to request upload token with error: {}", e),
))),
}
}
fn upload_file(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: &str,
user_id: &str,
file_path: &str,
) -> Result<i64, Box<dyn Error>> {
use std::fs::File;
use std::io::Read;
let file_name = std::path::Path::new(file_path)
.file_name()
.and_then(std::ffi::OsStr::to_str)
.ok_or("Invalid file name")?;
let file_size = std::fs::metadata(file_path)?.len();
match request_upload_token(
client,
canvas_info,
course_id,
assignment_id,
user_id,
file_name,
file_size,
) {
Ok((upload_url, upload_params)) => {
let mut file = File::open(file_path)?;
let mut file_content = Vec::new();
file.read_to_end(&mut file_content)?;
let mut form = Form::new();
for (key, value) in upload_params {
form = form.text(key, value);
}
form = form.file("file", file_path)?;
let response = client
.post(&upload_url)
.multipart(form)
.send()
.map_err(|e| format!("Failed to upload file: {}", e))?;
let json: Value = response
.json()
.map_err(|e| format!("Failed to parse upload file response: {}", e))?;
let file_id = json["id"]
.as_i64()
.ok_or("Missing id in upload file response")?;
Ok(file_id)
}
Err(e) => {
return Err(format!("Failed to request upload token: {}", e).into());
}
}
}
pub fn comment_with_file(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: u64,
student_id: u64,
file_path: Option<&str>,
comment_text: &str,
) -> Result<(), Box<dyn Error>> {
let user_id = student_id.to_string();
let assignment_id_str = assignment_id.to_string();
let file_ids = if let Some(path) = file_path {
let file_id = upload_file(
client,
canvas_info,
course_id,
&assignment_id_str,
&user_id,
path,
)
.map_err(|e| format!("Error in upload_file: {}", e))?;
Some(vec![file_id])
} else {
None
};
add_comment(
client,
canvas_info,
course_id,
&assignment_id_str,
&user_id,
comment_text,
file_ids,
)
.map_err(|e| format!("Error in add_comment: {}", e))?;
Ok(())
}
pub fn get_all_submissions(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: u64,
) -> Result<Vec<Value>, Box<dyn Error>> {
let url = format!(
"{}/courses/{}/assignments/{}/submissions",
canvas_info.url_canvas, course_id, assignment_id
);
let mut all_submissions = Vec::new();
let mut page = 1;
loop {
let params = vec![("page", page.to_string()), ("per_page", "100".to_string())];
let converted_params: Vec<(String, String)> = params
.into_iter()
.map(|(key, value)| (key.to_string(), value))
.collect();
match send_http_request(
client,
HttpMethod::Get,
&url,
canvas_info,
converted_params, ) {
Ok(response) => {
if response.status().is_success() {
let submissions_page: Vec<Value> = response.json()?;
if submissions_page.is_empty() {
break; }
all_submissions.extend(submissions_page);
page += 1; } else {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to fetch submissions with status: {}",
response.status()
),
)));
}
}
Err(e) => {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to fetch submissions with error: {}", e),
)));
}
}
}
Ok(all_submissions)
}
pub fn fetch_submissions_for_assignments<F>(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
user_id: u64,
assignment_ids: &[u64],
interaction: F,
) -> Result<Vec<Submission>, Box<dyn std::error::Error>>
where
F: Fn(),
{
let mut submissions = Vec::new();
for &assignment_id in assignment_ids {
let url = format!(
"{}/courses/{}/assignments/{}/submissions/{}",
canvas_info.url_canvas, course_id, assignment_id, user_id
);
let params = Vec::new();
interaction();
for attempt in 0..SYNC_ATTEMPT {
let response = send_http_request(
client,
HttpMethod::Get, &url, &canvas_info, params.clone(), );
match response {
Ok(response) => {
if response.status().is_success() {
let submission: Submission = response.json()?; submissions.push(submission);
} else {
let error_message = response.text()?;
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to fetch submissions with error: {} (a)",
error_message
),
)));
}
break;
}
Err(e) => {
if attempt == SYNC_ATTEMPT - 1 {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to fetch submissions with error: {} (b)", e),
)));
} else {
sleep(std::time::Duration::from_millis(100));
}
}
}
}
}
Ok(submissions)
}
pub fn fetch_students(course: &Course) -> Result<Vec<Student>, Box<dyn Error>> {
let url = format!(
"{}/courses/{}/users",
course.info.canvas_info.url_canvas, course.info.id
);
pub fn convert_json_to_student(
course_info: &Arc<CourseInfo>,
student: &serde_json::Value,
) -> Option<Student> {
let id = student["id"].as_u64()?;
let name = student["name"].as_str().map(String::from)?;
let email = student["email"].as_str().map(String::from)?;
Some(Student {
info: Arc::new(StudentInfo {
id,
name,
email,
course_info: Arc::clone(course_info),
}),
})
}
let mut all_students = Vec::new();
let mut page = 1;
let client = &Client::new();
loop {
let params = vec![
("enrollment_type[]", "student".to_string()),
("include[]", "email".to_string()),
("per_page", "150".to_string()),
("page", page.to_string()),
];
let converted_params: Vec<(String, String)> = params
.into_iter()
.map(|(key, value)| (key.to_string(), value))
.collect();
match send_http_request(
client,
HttpMethod::Get, &url,
&course.info.canvas_info,
converted_params, ) {
Ok(response) => {
if response.status().is_success() {
let students_page: Vec<serde_json::Value> = response.json()?;
if students_page.is_empty() {
break; }
all_students.extend(
students_page
.into_iter()
.filter_map(|student| convert_json_to_student(&course.info, &student)),
);
page += 1; } else {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to fetch students with status: {}",
response.status()
),
)));
}
}
Err(e) => {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to fetch students with error: {}", e),
)));
}
}
}
Ok(all_students)
}
pub fn fetch_assignments(course: &Course) -> Result<Vec<Assignment>, Box<dyn Error>> {
pub fn convert_json_to_assignment(
course_info: &Arc<CourseInfo>,
assignment: &serde_json::Value,
) -> Option<Assignment> {
let id = assignment["id"].as_u64()?;
let name = assignment["name"].as_str().map(String::from)?;
let description = assignment["description"].as_str().map(String::from);
Some(Assignment {
info: Arc::new(AssignmentInfo {
id,
name,
description,
course_info: Arc::clone(course_info),
}),
})
}
let url = format!(
"{}/courses/{}/assignments",
course.info.canvas_info.url_canvas, course.info.id
);
let mut all_assignments = Vec::new();
let mut page = 1;
let client = &reqwest::blocking::Client::new();
loop {
let params = vec![("page", page.to_string()), ("per_page", "100".to_string())];
let converted_params: Vec<(String, String)> = params
.into_iter()
.map(|(key, value)| (key.to_string(), value))
.collect();
match send_http_request(
client,
HttpMethod::Get,
&url,
&course.info.canvas_info,
converted_params, ) {
Ok(response) => {
if response.status().is_success() {
let assignments_page: Vec<serde_json::Value> = response.json()?;
if assignments_page.is_empty() {
break; }
all_assignments.extend(assignments_page.into_iter().filter_map(|assignment| {
convert_json_to_assignment(&course.info, &assignment)
}));
page += 1; } else {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to fetch assignments with status: {}",
response.status()
),
)));
}
}
Err(e) => {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to fetch assignments with error: {}", e),
)));
}
}
}
Ok(all_assignments)
}
pub fn update_assignment_score(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: u64,
student_id: u64,
new_score: Option<f64>,
) -> Result<(), Box<dyn Error>> {
let url = format!(
"{}/courses/{}/assignments/{}/submissions/{}",
canvas_info.url_canvas, course_id, assignment_id, student_id,
);
let body;
if let Some(new_score) = new_score {
body = serde_json::json!({
"submission": {
"posted_grade": new_score
}
});
} else {
body = serde_json::json!({
"submission": {
"posted_grade": ""
}
});
}
let mut attempt = SYNC_ATTEMPT;
loop {
match send_http_request(
client,
HttpMethod::Put(body.clone()), &url,
&canvas_info,
Vec::new(), ) {
Ok(response) => match response.status().is_success() {
true => return Ok(()),
false => {
if attempt == 0 {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to update score with status: {}", response.status()),
)));
}
}
},
Err(e) => {
if attempt == 0 {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to update score with error: {}", e),
)));
}
}
};
attempt -= 1;
sleep(std::time::Duration::from_millis(100));
}
}
pub fn comment_with_binary_file(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: u64,
student_id: u64,
file_name: Option<&str>,
file_content: Option<&Vec<u8>>,
comment_text: &str,
) -> Result<(), Box<dyn Error>> {
let user_id = student_id.to_string();
let assignment_id_str = assignment_id.to_string();
let file_ids = if let (Some(name), Some(content)) = (file_name, file_content) {
let mut attempts = 0;
let max_attempts = 3;
loop {
match upload_binary_file(
client,
canvas_info,
course_id,
&assignment_id_str,
&user_id,
name,
content,
) {
Ok(file_id) => break Some(vec![file_id]),
Err(e) => {
attempts += 1;
if attempts >= max_attempts {
return Err(format!("Error in upload_binary_file after {} attempts: {}", attempts, e).into());
}
sleep(std::time::Duration::from_secs(1)); }
}
}
} else {
None
};
add_comment(
client,
canvas_info,
course_id,
&assignment_id_str,
&user_id,
comment_text,
file_ids,
)
.map_err(|e| format!("Error in add_comment: {}", e))?;
Ok(())
}
fn upload_binary_file(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_id: &str,
user_id: &str,
file_name: &str,
file_content: &Vec<u8>,
) -> Result<i64, Box<dyn Error>> {
let file_size = file_content.len() as u64;
match request_upload_token(
client,
canvas_info,
course_id,
assignment_id,
user_id,
file_name,
file_size,
) {
Ok((upload_url, upload_params)) => {
let mut form = Form::new();
for (key, value) in upload_params {
form = form.text(key, value);
}
form = form.part(
"file",
Part::bytes(file_content.clone()).file_name(file_name.to_string()),
);
let response = client
.post(&upload_url)
.multipart(form)
.send()
.map_err(|e| format!("Failed to upload file: {}", e))?;
let json: Value = response
.json()
.map_err(|e| format!("Failed to parse upload file response: {}", e))?;
let file_id = json["id"]
.as_i64()
.ok_or("Missing id in upload file response")?;
Ok(file_id)
}
Err(e) => {
return Err(format!("Failed to request upload token: {}", e).into());
}
}
}
pub fn create_assignment(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
assignment_name: &str,
) -> Result<(), Box<dyn Error>> {
let url = format!("{}/courses/{}/assignments", canvas_info.url_canvas, course_id);
let body = json!({
"assignment": {
"name": assignment_name,
"points_possible": 10.0,
"grading_type": "points",
"submission_types": ["online_upload"],
"published": true,
}
});
match send_http_request(client, HttpMethod::Post(body), &url, canvas_info, vec![]) {
Ok(response) => {
if response.status().is_success() {
println!("Atividade '{}' criada com sucesso!", assignment_name);
Ok(())
} else {
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Falha ao criar atividade com status: {}",
response.status()
),
)))
}
}
Err(e) => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Falha ao criar atividade com erro: {}", e),
))),
}
}
pub fn create_announcement(
client: &Client,
canvas_info: &CanvasCredentials,
course_id: u64,
title: &str,
message: &str,
) -> Result<(), Box<dyn Error>> {
let url = format!("{}/courses/{}/discussion_topics", canvas_info.url_canvas, course_id);
let body = json!({
"title": title,
"message": message,
"is_announcement": true
});
match send_http_request(
client,
HttpMethod::Post(body),
&url,
canvas_info,
vec![],
) {
Ok(response) => {
if response.status().is_success() {
Ok(())
} else {
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create announcement with status: {}", response.status()),
)))
}
}
Err(e) => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create announcement with error: {}", e),
))),
}
}