diff --git a/cmd/lsf/main.go b/cmd/lsf/main.go index 2a5f283..bce99ee 100644 --- a/cmd/lsf/main.go +++ b/cmd/lsf/main.go @@ -19,6 +19,7 @@ import ( var ( sessionCache string username string + showGrades bool ) func init() { @@ -29,6 +30,7 @@ func init() { defaultCacheDir := path.Join(homeDir, ".cache/go-lsf/sessions") flag.StringVar(&sessionCache, "session-cache", defaultCacheDir, "path where the session tokens are located") flag.StringVar(&username, "username", "", "username to login with in lsf") + flag.BoolVar(&showGrades, "noten", false, "list grades") flag.Parse() err = os.MkdirAll(sessionCache, os.ModePerm) @@ -51,7 +53,16 @@ func main() { log.Fatal(err) } - fmt.Printf("s: %+v\n", s) + if showGrades { + noten, err := s.Noten() + if err != nil { + log.Fatal(err) + } + fmt.Println("Noten:") + for _, note := range noten { + fmt.Println(note) + } + } } func readUsername() (string, error) { @@ -69,7 +80,7 @@ func readPassword() (string, error) { fmt.Print("Password: ") b, err := terminal.ReadPassword(int(syscall.Stdin)) if err != nil { - return "", errors.Wrap(err, "could not read password") + return "", err } fmt.Print("\n") password = string(b) @@ -101,20 +112,16 @@ func session(username string) (*lsf.Session, error) { return nil, errors.Wrapf(err, "could not read session file %s", sessionPath) } sid := strings.TrimSuffix(string(b), "\n") - s := &lsf.Session{ - SID: sid, - } - valid, err := s.Valid() - if err != nil { - return nil, errors.Wrap(err, "could not check session") - } - if !valid { - fmt.Println("session is not valid") + s, err := lsf.NewSessionBySID(sid) + if err == lsf.ErrInvalSID { + log.Println("session is not valid") err := os.Remove(sessionPath) if err != nil { log.Println(errors.Wrap(err, "could not remove invalid session file")) } return login(username) + } else if err != nil { + return nil, errors.Wrap(err, "could not get session by existing sid") } return s, nil } @@ -122,7 +129,7 @@ func session(username string) (*lsf.Session, error) { func login(username string) (*lsf.Session, error) { password, err := readPassword() if err != nil { - return nil, errors.Wrap(err, "could not login") + return nil, errors.Wrap(err, "could not read password") } s, err := lsf.Login(username, password) if err != nil { diff --git a/module.go b/module.go index 7c2c9e6..e4bbafd 100644 --- a/module.go +++ b/module.go @@ -1,5 +1,6 @@ package lsf +// Modul hold information about a module type Modul struct { Nr int } diff --git a/noten.go b/noten.go index def852f..53afc87 100644 --- a/noten.go +++ b/noten.go @@ -1,10 +1,66 @@ package lsf import ( + "fmt" + "net/http" + "strings" + + "github.com/PuerkitoBio/goquery" "github.com/pkg/errors" - //"github.com/PuerkitoBio/goquery" ) -func (s *Session) Noten() (map[Modul]float32, error) { - return nil, errors.New("noten not implemented yet") +// Note is a grade with associated data like the related module +type Note struct { + Nr string + Name string + Semester string + Note string + Status string + CPs string + Versuch string +} + +func (n *Note) String() string { + return fmt.Sprintf("%s: %s", n.Name, n.Note) +} + +// Noten returns a list of the grades of all graded or signed up modules +func (s *Session) Noten() ([]*Note, error) { + var noten []*Note + client := &http.Client{} + url := "https://lsf.hs-worms.de/qisserver/rds?state=notenspiegelStudent&next=list.vm&nextdir=qispos/notenspiegel/student&createInfos=Y&struct=auswahlBaum&nodeID=auswahlBaum%7Cabschluss%3Aabschl%3D05%2Cstgnr%3D1%7Cstudiengang%3Astg%3D938%2Cpversion%3D2018&expand=0&asi=" + s.ASI + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, errors.Wrap(err, "could not prepare the request") + } + req.Header.Add("Cookie", fmt.Sprintf("JSESSIONID=%s", s.SID)) + resp, err := client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "could not do the request") + } + doc, err := goquery.NewDocumentFromResponse(resp) + if err != nil { + return nil, err + } + + // Find all tables. The last one contains the grades. Get all children rows. + rows := doc.Find("table").Last().Find("tr") + // The first three are the header, the average mark and a divider. So lets ignore them. + rows.Slice(3, rows.Length()).Each(func(_ int, row *goquery.Selection) { + vals := row.ChildrenFiltered("td").Map(func(i int, cell *goquery.Selection) string { + return strings.TrimSpace(cell.Text()) + }) + note := Note{ + Nr: vals[0], + Name: vals[1], + Semester: vals[2], + Note: vals[3], + Status: vals[4], + CPs: vals[5], + Versuch: vals[7], + } + noten = append(noten, ¬e) + }) + + return noten, nil } diff --git a/session.go b/session.go index 68a8e1a..8d20942 100644 --- a/session.go +++ b/session.go @@ -5,17 +5,30 @@ import ( "io/ioutil" "net/http" "net/url" + "regexp" "strings" "github.com/pkg/errors" ) +// Error variables +var ( + ErrInvalSID error = errors.New("invalid session id") +) + +// Session contains the session id and whatever this obscure asi token is. type Session struct { SID string + // ASI is a string you somehow have to pass to some endpoints. + ASI string } +// Valid checks if the session id of the session is valid func (s *Session) Valid() (bool, error) { client := &http.Client{} + // GET the logged in mail page. + // If correctly logged in there is a logout button. + // Otherwise there is a login button. req, err := http.NewRequest("GET", "https://lsf.hs-worms.de/qisserver/rds?state=user&type=8&topitem=functions&breadCrumbSource=portal", nil) if err != nil { return false, errors.Wrap(err, "could not prepare the request") @@ -39,6 +52,9 @@ func (s *Session) Valid() (bool, error) { return false, errors.New("unexpected response body") } +// Login tries to login with the given username and password. +// If successful a new Session with the session id and asi +// will be created and returned. func Login(username, password string) (*Session, error) { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { // don't follow redirects @@ -61,8 +77,13 @@ func Login(username, password string) (*Session, error) { if resp.StatusCode == 302 { for _, c := range resp.Cookies() { if c.Name == "JSESSIONID" { + asi, err := asi(c.Value) + if err != nil { + return nil, errors.Wrap(err, "could not get asi") + } return &Session{ SID: c.Value, + ASI: asi, }, nil } } @@ -73,3 +94,52 @@ func Login(username, password string) (*Session, error) { } return nil, errors.New("unexpected response status code") } + +// NewSessionBySID checks whether the given session id is valid and if so +// a new Session with the session id and asi will be created and returned. +func NewSessionBySID(sid string) (*Session, error) { + s := &Session{ + SID: sid, + } + valid, err := s.Valid() + if err != nil { + return nil, errors.Wrap(err, "could not check session") + } + if !valid { + return nil, ErrInvalSID + } + asi, err := asi(sid) + if err != nil { + return nil, errors.Wrap(err, "could not get asi") + } + return &Session{ + SID: sid, + ASI: asi, + }, nil +} + +func asi(sid string) (string, error) { + client := &http.Client{} + // GET Request with JESSIONID cookie to sitemap endpoint + req, err := http.NewRequest("GET", "https://lsf.hs-worms.de/qisserver/rds?state=sitemap&topitem=leer&breadCrumbSource=portal", nil) + if err != nil { + return "", errors.Wrap(err, "could not prepare the request") + } + req.Header.Add("Cookie", fmt.Sprintf("JSESSIONID=%s", sid)) + resp, err := client.Do(req) + if err != nil { + return "", errors.Wrap(err, "could not do the request") + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrap(err, "could not read the response body") + } + // The asi is in some links as GET parameter. + // Filter it with regexp + re := regexp.MustCompile(`asi=([^"]+)`) + match := re.FindSubmatch(b) + if len(match) != 2 { + return "", errors.New("no asi found") + } + return string(match[1]), nil +}