Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tildezero committed May 27, 2023
0 parents commit fd6ced3
Show file tree
Hide file tree
Showing 14 changed files with 1,119 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/discord.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/hac-cli-go.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# hac-cli

home access center command line interface

## usage
- build the executable: `go build`
- run the executable: `hac`
- setup credentials: `hac setup`
- view overview of classes (ids and averages): `hac overview`
- view all grades for a class: `hac class [id]` where id is from `hac overview`
133 changes: 133 additions & 0 deletions cmd/class.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package cmd

import (
"bytes"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strconv"
"strings"
"text/tabwriter"
"time"

"github.com/Delta456/box-cli-maker/v2"
"github.com/PuerkitoBio/goquery"
"github.com/briandowns/spinner"
"github.com/cheynewallace/tabby"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// classCmd represents the class command
var classCmd = &cobra.Command{
Use: "class [id]",
Short: "Get grades for a specifed class",
Long: `Get a full list of grades for a class based on its ID.
To get the ID for a class, run hac overview, and use the ID on the left column, and use that as an argument to hac class.
For example, if hac overview says that AP Calculus AB has an id of 1, to view a full set of grades for that class, one would run hac class 1`,
Run: func(cmd *cobra.Command, args []string) {
classID, err := strconv.Atoi(args[0])
if err != nil {
color.Red.Println("❌ Class ID must be a number!")
os.Exit(1)
}

if viper.GetString("username") == "" || viper.GetString("password") == "" || viper.GetString("url") == "" {
color.Red.Println("❌ Please define a username, password, and home access url in $HOME/.hac.yaml! (or, use hac setup to run a wizard that does it for you)")
os.Exit(1)
}

sp := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
sp.Suffix = color.Blue.Render(" Fetching data from HAC...")

sp.Start()
cj, err := cookiejar.New(nil)
cobra.CheckErr(err)
client := &http.Client{Jar: cj}
rvtReq, err := client.Get(fmt.Sprintf("%v/HomeAccess/Account/LogOn", viper.GetString("url")))
cobra.CheckErr(err)
defer rvtReq.Body.Close()

rvtDoc, err := goquery.NewDocumentFromReader(rvtReq.Body)
cobra.CheckErr(err)

sel := rvtDoc.Find("input[name='__RequestVerificationToken']")
rvt, ex := sel.Attr("value")
if !ex {
color.Red.Println("❌ Failed to fetch request token from HAC!")
os.Exit(1)
}

reqBody := url.Values{}
reqBody.Add("__RequestVerificationToken", rvt)
reqBody.Add("SCKTY00328510CustomEnabled", "False")
reqBody.Add("SCKTY00436568CustomEnabled", "False")
reqBody.Add("Database", "10")
reqBody.Add("VerificationOption", "UsernamePassword")
reqBody.Add("LogOnDetails.UserName", viper.GetString("username"))
reqBody.Add("LogOnDetails.Password", viper.GetString("password"))
reqBody.Add("tempUN", "")
reqBody.Add("tempPW", "")

loginResp, err := client.PostForm(fmt.Sprintf("%v/HomeAccess/Account/LogOn", viper.GetString("url")), reqBody)
client.Jar.SetCookies(loginResp.Request.URL, loginResp.Cookies())
cobra.CheckErr(err)

assignmentsResp, err := client.Get(fmt.Sprintf("%v/HomeAccess/Content/Student/Assignments.aspx", viper.GetString("url")))
client.Jar.SetCookies(assignmentsResp.Request.URL, assignmentsResp.Cookies())
cobra.CheckErr(err)

defer assignmentsResp.Body.Close()

assignments, err := goquery.NewDocumentFromReader(assignmentsResp.Body)
cobra.CheckErr(err)
sp.Stop()

buf := new(bytes.Buffer)
wtr := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0)

t := tabby.NewCustom(wtr)

t.AddHeader("Assigment", "Category", "Grade", "Due", "Assigned")

classHTML := assignments.Find(".AssignmentClass").Nodes[classID]
class := goquery.NewDocumentFromNode(classHTML)
class.Find("div[class='sg-content-grid'] tr[class='sg-asp-table-data-row']").Each(func(i int, s *goquery.Selection) {
tds := s.Find("td")
dueDate := strings.TrimSpace(tds.Eq(0).Text())
assignedDate := strings.TrimSpace(tds.Eq(1).Text())
category := strings.TrimSpace(tds.Eq(3).Text())
score := strings.TrimSpace(tds.Eq(4).Text())
total := strings.TrimSpace(tds.Eq(5).Text())
name := strings.TrimSpace(s.Find("a").Text())

if name != "" {
t.AddLine(name, category, fmt.Sprintf("%v/%v", score, total), dueDate, assignedDate)
}
})

t.Print()
bx := box.New(box.Config{Px: 0, Py: 0, Type: "Round", Color: "Cyan"})
className := strings.TrimSpace(
strings.TrimSpace(
class.Find("a[class='sg-header-heading']").Text(),
)[9:],
)
classAvg := strings.TrimSpace(
class.Find(
fmt.Sprintf("span[id='plnMain_rptAssigmnetsByCourse_lblHdrAverage_%v']", classID),
).Text(),
)[3:]
bx.Println(fmt.Sprintf("Grades for %v - Average %v", className, classAvg), buf.String())

},
}

func init() {
rootCmd.AddCommand(classCmd)
}
126 changes: 126 additions & 0 deletions cmd/overview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package cmd

import (
"bytes"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strconv"
"strings"
"text/tabwriter"
"time"

"github.com/Delta456/box-cli-maker/v2"
"github.com/PuerkitoBio/goquery"
"github.com/briandowns/spinner"
"github.com/cheynewallace/tabby"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// overviewCmd represents the overview command
var overviewCmd = &cobra.Command{
Use: "overview",
Short: "Overview of grades",
Long: `Get an overview of curent grades`,
Run: func(cmd *cobra.Command, args []string) {
if viper.GetString("username") == "" || viper.GetString("password") == "" || viper.GetString("url") == "" {
color.Red.Println("❌ Please define a username, password, and home access url in $HOME/.hac.yaml! (or, use hac setup to run a wizard that does it for you)")
os.Exit(1)
}
sp := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
sp.Suffix = color.Blue.Render(" Fetching data from HAC...")
sp.Start()
cj, err := cookiejar.New(nil)
cobra.CheckErr(err)
client := &http.Client{Jar: cj}
rvtReq, err := client.Get(fmt.Sprintf("%v/HomeAccess/Account/LogOn", viper.GetString("url")))
cobra.CheckErr(err)
defer rvtReq.Body.Close()

rvtDoc, err := goquery.NewDocumentFromReader(rvtReq.Body)
cobra.CheckErr(err)

sel := rvtDoc.Find("input[name='__RequestVerificationToken']")
rvt, ex := sel.Attr("value")
if !ex {
color.Red.Println("❌ Failed to fetch Request Token from HAC!")
os.Exit(1)
}

reqBody := url.Values{}
reqBody.Add("__RequestVerificationToken", rvt)
reqBody.Add("SCKTY00328510CustomEnabled", "False")
reqBody.Add("SCKTY00436568CustomEnabled", "False")
reqBody.Add("Database", "10")
reqBody.Add("VerificationOption", "UsernamePassword")
reqBody.Add("LogOnDetails.UserName", viper.GetString("username"))
reqBody.Add("LogOnDetails.Password", viper.GetString("password"))
reqBody.Add("tempUN", "")
reqBody.Add("tempPW", "")

loginResp, err := client.PostForm(fmt.Sprintf("%v/HomeAccess/Account/LogOn", viper.GetString("url")), reqBody)
client.Jar.SetCookies(loginResp.Request.URL, loginResp.Cookies())
cobra.CheckErr(err)

assignmentsResp, err := client.Get(fmt.Sprintf("%v/HomeAccess/Content/Student/Assignments.aspx", viper.GetString("url")))
client.Jar.SetCookies(assignmentsResp.Request.URL, assignmentsResp.Cookies())
cobra.CheckErr(err)

defer assignmentsResp.Body.Close()

assignments, err := goquery.NewDocumentFromReader(assignmentsResp.Body)
cobra.CheckErr(err)
sp.Stop()

buf := new(bytes.Buffer)
wtr := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0)

t := tabby.NewCustom(wtr)

t.AddHeader("Class ID", "Name", "Average")

assignments.Find(".AssignmentClass").Each(func(i int, s *goquery.Selection) {
name := s.Find("a[class='sg-header-heading']").Text()
avg := s.Find(fmt.Sprintf("span[id='plnMain_rptAssigmnetsByCourse_lblHdrAverage_%v']", i)).Text()
t.AddLine(i, strings.TrimSpace(strings.TrimSpace(name)[9:]), colorGrade(strings.TrimSpace(avg[3:])))
})

t.Print()
bx := box.New(box.Config{Px: 0, Py: 0, Type: "Round", Color: "Cyan"})
bx.Println("", buf.String())

},
}

func colorGrade(avg string) string {
numAvg, _ := strconv.Atoi(avg)
switch true {
case numAvg >= 90:
return color.Green.Render(avg)
case numAvg >= 80:
return color.Blue.Render(avg)
case numAvg >= 70:
return color.Yellow.Render(avg)
default:
return color.Red.Render(avg)
}

}

func init() {
rootCmd.AddCommand(overviewCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// overviewCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// overviewCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
Loading

0 comments on commit fd6ced3

Please sign in to comment.