Add Noten parsing and rework some existing code

This commit is contained in:
Marcel Transier 2019-10-06 19:20:57 +02:00
parent b7bea8cf09
commit eb9f2ed4ce
4 changed files with 149 additions and 15 deletions

View File

@ -19,6 +19,7 @@ import (
var ( var (
sessionCache string sessionCache string
username string username string
showGrades bool
) )
func init() { func init() {
@ -29,6 +30,7 @@ func init() {
defaultCacheDir := path.Join(homeDir, ".cache/go-lsf/sessions") defaultCacheDir := path.Join(homeDir, ".cache/go-lsf/sessions")
flag.StringVar(&sessionCache, "session-cache", defaultCacheDir, "path where the session tokens are located") flag.StringVar(&sessionCache, "session-cache", defaultCacheDir, "path where the session tokens are located")
flag.StringVar(&username, "username", "", "username to login with in lsf") flag.StringVar(&username, "username", "", "username to login with in lsf")
flag.BoolVar(&showGrades, "noten", false, "list grades")
flag.Parse() flag.Parse()
err = os.MkdirAll(sessionCache, os.ModePerm) err = os.MkdirAll(sessionCache, os.ModePerm)
@ -51,7 +53,16 @@ func main() {
log.Fatal(err) 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) { func readUsername() (string, error) {
@ -69,7 +80,7 @@ func readPassword() (string, error) {
fmt.Print("Password: ") fmt.Print("Password: ")
b, err := terminal.ReadPassword(int(syscall.Stdin)) b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
return "", errors.Wrap(err, "could not read password") return "", err
} }
fmt.Print("\n") fmt.Print("\n")
password = string(b) 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) return nil, errors.Wrapf(err, "could not read session file %s", sessionPath)
} }
sid := strings.TrimSuffix(string(b), "\n") sid := strings.TrimSuffix(string(b), "\n")
s := &lsf.Session{ s, err := lsf.NewSessionBySID(sid)
SID: sid, if err == lsf.ErrInvalSID {
} log.Println("session is not valid")
valid, err := s.Valid()
if err != nil {
return nil, errors.Wrap(err, "could not check session")
}
if !valid {
fmt.Println("session is not valid")
err := os.Remove(sessionPath) err := os.Remove(sessionPath)
if err != nil { if err != nil {
log.Println(errors.Wrap(err, "could not remove invalid session file")) log.Println(errors.Wrap(err, "could not remove invalid session file"))
} }
return login(username) return login(username)
} else if err != nil {
return nil, errors.Wrap(err, "could not get session by existing sid")
} }
return s, nil return s, nil
} }
@ -122,7 +129,7 @@ func session(username string) (*lsf.Session, error) {
func login(username string) (*lsf.Session, error) { func login(username string) (*lsf.Session, error) {
password, err := readPassword() password, err := readPassword()
if err != nil { 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) s, err := lsf.Login(username, password)
if err != nil { if err != nil {

View File

@ -1,5 +1,6 @@
package lsf package lsf
// Modul hold information about a module
type Modul struct { type Modul struct {
Nr int Nr int
} }

View File

@ -1,10 +1,66 @@
package lsf package lsf
import ( import (
"fmt"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/pkg/errors" "github.com/pkg/errors"
//"github.com/PuerkitoBio/goquery"
) )
func (s *Session) Noten() (map[Modul]float32, error) { // Note is a grade with associated data like the related module
return nil, errors.New("noten not implemented yet") 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, &note)
})
return noten, nil
} }

View File

@ -5,17 +5,30 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"strings" "strings"
"github.com/pkg/errors" "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 { type Session struct {
SID string 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) { func (s *Session) Valid() (bool, error) {
client := &http.Client{} 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) req, err := http.NewRequest("GET", "https://lsf.hs-worms.de/qisserver/rds?state=user&type=8&topitem=functions&breadCrumbSource=portal", nil)
if err != nil { if err != nil {
return false, errors.Wrap(err, "could not prepare the request") 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") 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) { func Login(username, password string) (*Session, error) {
client := &http.Client{ client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { // don't follow redirects 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 { if resp.StatusCode == 302 {
for _, c := range resp.Cookies() { for _, c := range resp.Cookies() {
if c.Name == "JSESSIONID" { if c.Name == "JSESSIONID" {
asi, err := asi(c.Value)
if err != nil {
return nil, errors.Wrap(err, "could not get asi")
}
return &Session{ return &Session{
SID: c.Value, SID: c.Value,
ASI: asi,
}, nil }, nil
} }
} }
@ -73,3 +94,52 @@ func Login(username, password string) (*Session, error) {
} }
return nil, errors.New("unexpected response status code") 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
}