aplus

A+Attendance CLI utility via Instructure Canvas
Log | Files | Refs | README | LICENSE

aplus.go (7027B)


      1 package main
      2 
      3 import (
      4 	"encoding/json"
      5 	"fmt"
      6 	"html"
      7 	"io"
      8 	"net/http"
      9 	"net/url"
     10 	"regexp"
     11 	"strconv"
     12 	"strings"
     13 	"time"
     14 )
     15 
     16 func submit_code(course_code int, attendance_code string) bool {
     17 	cur_body := launch_aplus(course_code)
     18 	if cur_body == "" {
     19 		fmt.Println("Unknown error")
     20 		return false
     21 	} else if cur_body == "Roles not supported" {
     22 		fmt.Println("Cannot access course", course_code, "(canvas error \"Roles not supported\")")
     23 		return false
     24 	}
     25 
     26 	if verbose {
     27 		fmt.Printf("%s", cur_body)
     28 	}
     29 
     30 	if strings.Contains(cur_body, "Student not known") {
     31 		fmt.Println("Student not known. This course must first be visited by an instructor for students to have access.")
     32 		return false
     33 	}
     34 
     35 	current_url := fmt.Sprintf("%s/student/", base_aplus_link)
     36 	// the links to submit the code for a class is under the dayPanel div
     37 	daypanel_start := strings.Index(cur_body, "<div class=\"dayPanel\"")
     38 
     39 	if !strings.Contains(cur_body[daypanel_start:], "<a href=\"./?module") {
     40 		fmt.Println("No code submitted. Did the professor generate the code or is the class in session?")
     41 		return false
     42 	}
     43 
     44 	// link_start - link_end is to extract the URL
     45 	// TODO: make this more elegant to allow multiple of these links
     46 	//       (maybe use an html parser)
     47 	// 	 or at least use extract_attribute()
     48 	link_start := daypanel_start
     49 	link_start += strings.Index(cur_body[link_start:], "<a href=\"./?module")
     50 	link_start += len("<a href=\"")
     51 	link_end := link_start
     52 	link_end += strings.Index(cur_body[link_end:], "\"")
     53 
     54 	link_url := current_url + cur_body[link_start:link_end]
     55 	link_url = html.UnescapeString(link_url)
     56 
     57 	if verbose {
     58 		fmt.Println("!!!")
     59 		fmt.Printf("%i %i %i\n", daypanel_start, link_start, link_end)
     60 		fmt.Println(link_url)
     61 		fmt.Println("!!!")
     62 	}
     63 
     64 	resp, _ := client.Get(link_url)
     65 	body, _ := io.ReadAll(resp.Body)
     66 
     67 	form_str := get_form_from_request_body(body)
     68 	form_values := parse_form(form_str)
     69 	form_values["ctl01$sessionCode"][0] = attendance_code
     70 
     71 	resp, _ = client.PostForm(link_url, form_values)
     72 	body, _ = io.ReadAll(resp.Body)
     73 
     74 	if strings.Index(string(body), "ctl01_errorMessage") != -1 {
     75 		errormsg_start := strings.Index(string(body), "ctl01_errorMessage")
     76 		errormsg_start += strings.Index(string(body)[errormsg_start:], "Text\">")
     77 		errormsg_start += len("Text\">")
     78 		errormsg_end := errormsg_start
     79 		errormsg_end += strings.Index(string(body)[errormsg_start:], "<")
     80 		fmt.Println(string(body)[errormsg_start:errormsg_end])
     81 	} else if strings.Index(string(body), "ctl02_codeSuccessMessage") != -1 {
     82 		fmt.Println("Code successfully recorded")
     83 	}
     84 
     85 	return true
     86 }
     87 
     88 func launch_aplus(course_code int) string {
     89 	err := init_aplus_toolid(course_code)
     90 	if err != nil {
     91 		fmt.Println("Failed to get A+ base URL and tool ID. Try again with a different course code.")
     92 		fmt.Printf("Error: %s\n", err)
     93 		return ""
     94 	}
     95 
     96 	aplus_link := fmt.Sprintf("%s/courses/%d/external_tools/sessionless_launch?id=%d&access_token=%s",
     97 		base_link, course_code, external_tools_code, token)
     98 	aplus := get_aplus(token, aplus_link, client)
     99 
    100 	resp, _ := client.Get(aplus)
    101 	body, _ := io.ReadAll(resp.Body)
    102 
    103 	form_str := get_form_from_request_body(body)
    104 	form_values := parse_form(form_str)
    105 
    106 	resp, _ = client.PostForm(base_aplus_link, form_values)
    107 	body, _ = io.ReadAll(resp.Body)
    108 
    109 	if strings.Index(string(body), "Roles not supported") != -1 {
    110 		return "Roles not supported"
    111 	} else {
    112 		return string(body)
    113 	}
    114 }
    115 
    116 func get_aplus(token string, link string, client http.Client) string {
    117 	resp, _ := client.Get(link)
    118 
    119 	body, _ := io.ReadAll(resp.Body)
    120 
    121 	var aplus Aplus
    122 	json.Unmarshal(body, &aplus)
    123 
    124 	return aplus.URL
    125 }
    126 
    127 func get_form_from_request_body(req_body []byte) string {
    128 	body_str := string(req_body)
    129 	form_start := strings.Index(body_str, "<form")
    130 	form_end := strings.Index(body_str, "</form>") + 7
    131 	form_html := req_body[form_start:form_end]
    132 
    133 	return string(form_html)
    134 }
    135 
    136 // parse_form extracts form fields and values from the given HTML form string.
    137 func parse_form(form_html string) url.Values {
    138 	form_values := make(url.Values)
    139 	inputs := strings.Split(form_html, "<input")
    140 
    141 	for _, input := range inputs {
    142 		// Extract field name and value
    143 		name := extract_attribute(input, "name")
    144 		value := html.UnescapeString(extract_attribute(input, "value"))
    145 
    146 		if name != "" {
    147 			form_values.Add(name, value)
    148 		}
    149 	}
    150 
    151 	return form_values
    152 }
    153 
    154 func extract_attribute(input string, attribute string) string {
    155 	start := strings.Index(input, attribute+"=\"")
    156 	if start == -1 {
    157 		start = strings.Index(input, attribute+"='")
    158 	}
    159 
    160 	if start == -1 {
    161 		return ""
    162 	}
    163 
    164 	start += len(attribute) + 2
    165 
    166 	end := strings.Index(input[start:], "\"")
    167 	if end == -1 {
    168 		end = strings.Index(input[start:], "'")
    169 	}
    170 
    171 	if end == -1 {
    172 		return ""
    173 	}
    174 
    175 	return input[start : start+end]
    176 }
    177 
    178 func submit_code_sans_course(attendance_code string) {
    179 	courses := list_all_courses(true)
    180 
    181 	for _, course := range courses {
    182 		fmt.Println("Checking with course", course.ID%10000)
    183 		if submit_code(int(course.ID%10000), attendance_code) == true {
    184 			fmt.Println("Submitted code to course", course.ID%10000)
    185 			return
    186 		}
    187 	}
    188 
    189 	fmt.Println("Could not submit code", attendance_code, "to any course")
    190 }
    191 
    192 func timetable(course_code int) {
    193 	cur_body := launch_aplus(course_code)
    194 	if cur_body == "" {
    195 		fmt.Println("Unknown error")
    196 		return
    197 	} else if cur_body == "Roles not supported" {
    198 		fmt.Println("Cannot access course", course_code, "(canvas error \"Roles not supported\")")
    199 		return
    200 	}
    201 
    202 	if verbose {
    203 		fmt.Printf("%s", cur_body)
    204 	}
    205 
    206 	if strings.Contains(cur_body, "Student not known") {
    207 		fmt.Println("Student not known. This course must first be visited by an instructor for students to have access.")
    208 		return
    209 	}
    210 
    211 	current_url := fmt.Sprintf("%s/student/", base_aplus_link)
    212 	tt_start := strings.Index(cur_body, "<div class=\"stv_tt_container\"")
    213 
    214 	link_start := tt_start
    215 	link_start += strings.Index(cur_body[link_start:], "<a href=\"./?canvasCourse")
    216 	link_start += len("<a href=\"")
    217 	link_end := link_start
    218 	link_end += strings.Index(cur_body[link_end:], "\"")
    219 
    220 	link_url := current_url + cur_body[link_start:link_end]
    221 	link_url = html.UnescapeString(link_url)
    222 
    223 	resp, _ := client.Get(link_url)
    224 	body, _ := io.ReadAll(resp.Body)
    225 
    226 	tt_full := string(body)[(strings.Index(string(body), "<div class=\"stv_tt_container\"")):]
    227 	tt_full = tt_full[:strings.Index(tt_full, "\n")]
    228 
    229 	re_date := regexp.MustCompile("Date\\.UTC\\([^\\)]+")
    230 	re_status := regexp.MustCompile("title=\"\">[^<]*")
    231 
    232 	dates, statuses := re_date.FindAllString(tt_full, -1), re_status.FindAllString(tt_full, -1)
    233 	for i := range dates {
    234 		jsdate := strings.Split(dates[i][9:], ",")
    235 		year, _ := strconv.Atoi(jsdate[0])
    236 		month, _ := strconv.Atoi(jsdate[1])
    237 		day, _ := strconv.Atoi(jsdate[2])
    238 		hour, _ := strconv.Atoi(jsdate[3])
    239 		min, _ := strconv.Atoi(jsdate[4])
    240 		sec, _ := strconv.Atoi(jsdate[5])
    241 		date := time.Date(year, time.Month(month+1), day, hour, min, sec, 0, time.UTC).Local()
    242 
    243 		fmt.Println(date.Format("Mon Jan 02 15:04:05 MST 2006") + ": " + strings.Replace(statuses[i], "title=\"\">", "", 1))
    244 	}
    245 }