// API for querying and updating a moodle server // // api := moodle.NewMoodleApi("https://moodle.example.com/moodle/", "a0092ba9a9f5b45cdd2f01d049595bfe91", l) // // // Search moodle courses // courses, _ := api.GetCourses("History") // if courses != nil { // for _, i := range *courses { // fmt.Printf("%s\n", i.Code) // } // } // // // Search users // people, err := api.GetPeopleByAttribute("email", "%") // if err != nil { // l.Error("%v", err) // return // } // fmt.Println("People:") // for _, p := range *people { // // Do something // } // package moodle import ( "bytes" crand "crypto/rand" "crypto/tls" "encoding/base64" "encoding/binary" "encoding/json" "errors" "fmt" "io" "io/ioutil" "math/rand" "net/smtp" "net/url" "sort" "strconv" "strings" "time" ) // API Documentation // https://docs.moodle.org/dev/Web_service_API_functions type MoodleApi struct { base string token string smtpUser string smtpPassword string smtpHost string smtpPort int smtpFromName string smtpFromEmail string log MoodleLogger fetch LookupUrl } func NewMoodleApi(base string, token string) *MoodleApi { if base != "" { if !strings.HasSuffix(base, "/") { base = base + "/" } } return &MoodleApi{ base: base, token: token, log: &NilMoodleLogger{}, fetch: &DefaultLookupUrl{}, } } func (m *MoodleApi) SetSmtpSettings(host string, port int, user, password string, fromName, fromEmail string) { m.smtpUser = user m.smtpPassword = password m.smtpHost = host m.smtpPort = port m.smtpFromName = fromName m.smtpFromEmail = fromEmail } func (m *MoodleApi) MoodleUrl() string { return m.base } type Course struct { MoodleId int64 `json:"id,omitempty"` Code string `json:"shortname,omitempty"` Name string `json:"fullname,omitempty"` Summary string `json:",omitempty"` Assignments []*Assignment `json:",omitempty"` Roles []*Role `json:",omitempty"` Created *time.Time `json:"-"` Start *time.Time `json:",omitempty"` End *time.Time `json:",omitempty"` } type Person struct { MoodleId int64 `json:",omitempty"` Username string `json:",omitempty"` Email string `json:",omitempty"` FirstName string `json:",omitempty"` LastName string `json:",omitempty"` ProfileImageUrl string `json:"profileimageurl,omitempty"` ProfileImageUrlSmall string `json:"profileimageurlsmall,omitempty"` Suspended bool Created *time.Time `json:",omitempty"` Roles []*Role `json:"role,omitempty"` CustomField []CustomField `json:"customfields,omitempty"` } func (p *Person) Field(name string) string { for _, c := range p.CustomField { if c.Name == name { return c.Value } } return "" } func (p *Person) SetField(name, value string) { for i, c := range p.CustomField { if c.Name == name { p.CustomField[i].Value = value return } } p.CustomField = append(p.CustomField, CustomField{Name: name, Value: value}) return } type Role struct { Person *Person `json:",omitempty"` Course *Course `json:",omitempty"` Role *RoleInfo Enrolled *time.Time GradeInfo []GradeInfo `json:",omitempty"` GradeOverride bool GradeOverrideValue float64 GradeFinal float64 } type Submission struct { MoodleId int64 Person Person Submitted *time.Time Extension *time.Time } type Assignment struct { MoodleId int64 `json:",omitempty"` Name string `json:",omitempty"` Due *time.Time `json:",omitempty"` Weight float64 `json:",omitempty"` Description string `json:",omitempty"` Submissions []Submission `json:",omitempty"` Type string `json:",omitempty"` Updated *time.Time `json:",omitempty"` } type RoleInfo struct { Name string `json:",omitempty"` MoodleId int64 `json:"-"` } type GradeInfo struct { Grade float64 `json:",omitempty"` GradeMin float64 `json:",omitempty"` GradeMax float64 `json:",omitempty"` Assignment *Assignment `json:",omitempty"` Excluded bool Updated *time.Time `json:",omitempty"` } type ByCourseCode []Course func (a ByCourseCode) Len() int { return len(a) } func (a ByCourseCode) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByCourseCode) Less(i, j int) bool { return a[i].Code < a[j].Code } func readError(body string) string { if !strings.HasPrefix(body, "{\"exception\":\"") || strings.Index(body, "\"message\":\"") < 0 { return "" } type Response struct { Message string `json:"message"` Exception string `json:"exception"` ErrorCode string `json:"errorcode"` DebugInfo string `json:"debuginfo"` } var response Response if err := json.Unmarshal([]byte(body), &response); err != nil { return "" } if response.Message != "" { return response.Message } return response.Exception } // Get Moodle Account details matching by username. Returns nil if not found. Returns error if multiple matches are found. func (m *MoodleApi) GetPersonByUsername(username string) (*Person, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&field=username&values[0]=%s", m.base, m.token, "core_user_get_users_by_field", url.QueryEscape(username)) body, _, _, err := m.fetch.GetUrl(url) m.log.Debug("Fetch: %s", url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return nil, errors.New(message) } type Result struct { Id int64 `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` Email string `json:"email"` Username string `json:"username"` CustomFields []CustomField `json:"customfields"` } var results []Result if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } if len(results) > 1 { return nil, errors.New("Multiple moodle accounts match this username") } var person *Person for _, i := range results { person = &Person{MoodleId: i.Id, FirstName: i.FirstName, LastName: i.LastName, Email: i.Email, Username: i.Username} for _, c := range i.CustomFields { person.CustomField = append(person.CustomField, CustomField{Name: c.Name, Value: c.Value}) } break } return person, nil } type MoodleLogger interface { Debug(message string, items ...interface{}) error } type NilMoodleLogger struct { } func (ml *NilMoodleLogger) Debug(message string, items ...interface{}) error { return nil } func (m *MoodleApi) SetLogger(l MoodleLogger) { m.log = l } // Get Moodle Account details matching by moodle id. Returns nil if not found. func (m *MoodleApi) GetPersonByMoodleId(id int64) (*Person, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&field=id&values[0]=%d", m.base, m.token, "core_user_get_users_by_field", id) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return nil, errors.New(message) } type Result struct { Id int64 `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` Email string `json:"email"` Username string `json:"username"` CustomFields []CustomField `json:"customfields"` } var results []Result if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } if len(results) > 1 { return nil, errors.New("Multiple moodle accounts match this username") } var person *Person for _, i := range results { person = &Person{MoodleId: i.Id, FirstName: i.FirstName, LastName: i.LastName, Email: i.Email, Username: i.Username} for _, c := range i.CustomFields { person.CustomField = append(person.CustomField, CustomField{Name: c.Name, Value: c.Value}) } break } return person, nil } type UploadResponse struct { ItemId int64 `json:"itemid"` } // SetProfilePicture uploads a draft file, set is as a profile picture, then removes the draft file func (m *MoodleApi) SetProfilePicture(userMoodleId int64, r io.Reader) error { now := time.Now() data, err := ioutil.ReadAll(r) if err != nil { return err } img := base64.StdEncoding.EncodeToString(data) url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&filearea=draft&instanceid=%d&component=user&filepath=/&contextlevel=user&filename=profilepic%s.jpg&filecontent=%s&itemid=%d", m.base, m.token, "core_files_upload", userMoodleId, now.Format("20060102150405"), url.QueryEscape(img), userMoodleId) // 1. Upload a draft file //url := fmt.Sprintf("%swebservice/upload.php?token=%s&wsfunction=%s&moodlewsrestformat=json&filearea=draft&instanceid=%d&component=user&filepath=/&contextlevel=user&filename=profilepic%s.jpg&itemid=%d", m.base, m.token, "core_files_upload", userMoodleId, now.Format("20060102150405"), userMoodleId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } fmt.Println(body) var draftFileId int64 = 0 if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } if strings.Index(body, "\"itemid\":") > 0 { var u UploadResponse if err := json.Unmarshal([]byte(body), &u); err != nil { return errors.New("Server returned unexpected response. " + err.Error()) } draftFileId = u.ItemId } else { return errors.New("Server returned unexpected response: " + body) } fmt.Println(draftFileId) // 2. Update the profile picture url = fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&draftitemid=%d&userid=%d", m.base, m.token, "core_user_update_picture", draftFileId, userMoodleId) m.log.Debug("Fetch: %s", url) body, _, _, err = m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } if strings.TrimSpace(body) != "null" { return errors.New("Server returned unexpected response: " + body) } fmt.Println("profile set") // 3. Remove the draft file /* url = fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&draftitemid=0&delete=1", m.base, m.token, "core_user_update_picture") m.log.Debug("Fetch: %s", url) body, _, _, err = m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } if strings.TrimSpace(body) != "null" { return errors.New("Server returned unexpected response: " + body) }*/ return nil } // Set the password for a moodle account. Password must match moodle password policy. func (m *MoodleApi) ResetPassword(moodleId int64, password string) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&users[0][id]=%d&users[0][password]=%s", m.base, m.token, "core_user_update_users", moodleId, url.QueryEscape(password)) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } if strings.TrimSpace(body) != "null" { return errors.New("Server returned unexpected response: " + body) } return nil } // Get moodle account matching by email address. func (m *MoodleApi) GetPersonByEmail(email string) (*Person, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&field=email&values[0]=%s", m.base, m.token, "core_user_get_users_by_field", url.QueryEscape(email)) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return nil, errors.New(message + ". " + url) } type Result struct { Id int64 `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` Email string `json:"email"` Username string `json:"username"` ProfileImageUrl string `json:"profileimageurl,omitempty"` ProfileImageUrlSmall string `json:"profileimageurlsmall,omitempty"` CustomFields []CustomField `json:"customfields"` } var results []Result if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } people := make([]Person, 0, len(results)) for _, i := range results { if strings.Index(i.ProfileImageUrl, "gravatar") > 0 { i.ProfileImageUrl = "" i.ProfileImageUrlSmall = "" } p := Person{MoodleId: i.Id, FirstName: i.FirstName, LastName: i.LastName, Email: i.Email, Username: i.Username, ProfileImageUrl: i.ProfileImageUrl, ProfileImageUrlSmall: i.ProfileImageUrlSmall} for _, c := range i.CustomFields { p.CustomField = append(p.CustomField, CustomField{Name: c.Name, Value: c.Value}) } people = append(people, p) } if len(people) == 0 { return nil, nil } if len(people) == 1 { return &people[0], nil } return nil, errors.New("Multiple moodle accounts match this email address") } const rst = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789" func NewCryptoSeededSource() rand.Source { var seed int64 binary.Read(crand.Reader, binary.BigEndian, &seed) return rand.NewSource(seed) } // RandomPassword differs from RandomString in that it ensures we dont have // a series of repeated or incrementing characters, and ensures we have at // least one uppercase lowercase, and number character. func RandomPassword() string { size := 10 random := rand.New(NewCryptoSeededSource()) bytes := make([]byte, size) hasNumber := false hasLowercase := false hasUppercase := false var last byte = 0 for i := 0; i < size; i++ { if size > 2 && i == size-1 && hasUppercase == false { c := 'B' + uint8(random.Int31n(25)) if c == 'O' { c = 'A' } bytes[i] = c continue } if size > 3 && i == size-2 && hasLowercase == false { c := 'b' + uint8(random.Int31n(25)) if c == 'l' { c = 'a' } bytes[i] = c continue } if size > 4 && i == size-3 && hasNumber == false { c := '2' + uint8(random.Int31n(8)) bytes[i] = c continue } x := random.Intn(len(rst)) c := rst[x] if c == last || c == last+1 { i = i - 1 continue } if c <= '9' { hasNumber = true } else if c <= 'Z' { hasUppercase = true } else { hasLowercase = true } bytes[i] = c last = c } s := string(bytes) return s[0:5] + "-" + s[5:] } // Reset the password for a moodle account, and email the password to the user func (m *MoodleApi) ResetPasswordWithEmail(email string) error { p, err := m.GetPersonByEmail(email) if err != nil { return err } if p == nil { return errors.New("Email address not found in moodle") } pwd := RandomPassword() err = m.ResetPassword(p.MoodleId, pwd) if err != nil { return errors.New("Password Reset failed. " + err.Error()) } if m.smtpHost == "" || m.smtpPort == 0 { return errors.New("ResetPasswordWithEmail() requires smtp host and port to be specified.") } if m.smtpUser == "" || m.smtpPassword == "" { return errors.New("ResetPasswordWithEmail() requires smtp user and password to be specified.") } if m.smtpFromName == "" || m.smtpFromEmail == "" { return errors.New("ResetPasswordWithEmail() requires smtp from name and email to be specified.") } var w bytes.Buffer w.Write([]byte(fmt.Sprintf("From: %s <%s>\r\n", m.smtpFromName, m.smtpFromEmail))) w.Write([]byte(fmt.Sprintf("To: %s\r\n", p.FirstName+" "+p.LastName+" <"+p.Email+">"))) w.Write([]byte(fmt.Sprintf("Subject: Welcome to the Planetshakers College moodle\r\n"))) w.Write([]byte("Content-Type: text/plain; charset=utf-8; format=flowed\r\n")) w.Write([]byte("Content-Transfer-Encoding: 8bit\r\n")) w.Write([]byte("\r\n")) w.Write([]byte("Hi " + p.FirstName + ",\r\n")) w.Write([]byte("\r\n")) w.Write([]byte("Welcome to the Planetshakers College Moodle, You can sign-in using the details below:\r\n")) w.Write([]byte("\r\n")) w.Write([]byte(" URL: " + m.base + "\r\n")) w.Write([]byte(" Username: " + p.Email + "\r\n")) w.Write([]byte(" Password: " + pwd + "\r\n")) w.Write([]byte("\r\n")) w.Write([]byte("If you have any difficulties with moodle access, please contact college@planetshakers.com\r\n")) w.Write([]byte("\r\n")) w.Write([]byte("God bless,\r\n")) w.Write([]byte("Planetshakers College\r\n")) w.Write([]byte("\r\n")) msg := w.Bytes() fmt.Println(string(msg)) var auth smtp.Auth if m.smtpUser != "" && m.smtpPassword != "" { auth = smtp.PlainAuth("", m.smtpUser, m.smtpPassword, m.smtpHost) } // TLS config tlsconfig := &tls.Config{ InsecureSkipVerify: true, ServerName: m.smtpHost, } // Here is the key, you need to call tls.Dial instead of smtp.Dial // for smtp servers running on 465 that require an ssl connection // from the very beginning (no starttls) conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", m.smtpHost, m.smtpPort), tlsconfig) if err != nil { return errors.New(fmt.Sprintf("tls.Dial(\"%s:%d\") failed: %v", m.smtpHost, m.smtpPort, err)) } c, err := smtp.NewClient(conn, m.smtpHost) if err != nil { return errors.New(fmt.Sprintf("SMTP.NewClient() failed: %v", err)) } if err = c.Auth(auth); err != nil { return errors.New(fmt.Sprintf("SMTP.Auth() failed: %v", err)) } if err = c.Mail(m.smtpFromEmail); err != nil { return errors.New(fmt.Sprintf("SMTP.Mail() failed: %v", err)) } if err = c.Rcpt(p.Email); err != nil { return errors.New(fmt.Sprintf("SMTP.Rcpt() failed: %v", err)) } w1, err := c.Data() if err != nil { return errors.New(fmt.Sprintf("SMTP.Data() failed: %v", err)) } _, err = w1.Write([]byte(msg)) if err != nil { return err } err = w1.Close() if err != nil { return errors.New(fmt.Sprintf("SMTP.Close() failed: %v", err)) } c.Quit() return nil } // Reset the password for a moodle account, and email the password to the user func (m *MoodleApi) WritingResetPasswordWithEmail(email string) error { p, err := m.GetPersonByEmail(email) if err != nil { return err } if p == nil { return errors.New("Email address not found in moodle") } pwd := RandomPassword() err = m.ResetPassword(p.MoodleId, pwd) if err != nil { return err } if m.smtpHost == "" || m.smtpPort == 0 { return errors.New("ResetPasswordWithEmail() requires smtp host and port to be specified.") } if m.smtpUser == "" || m.smtpPassword == "" { return errors.New("ResetPasswordWithEmail() requires smtp user and password to be specified.") } if m.smtpFromName == "" || m.smtpFromEmail == "" { return errors.New("ResetPasswordWithEmail() requires smtp from name and email to be specified.") } var w bytes.Buffer w.Write([]byte(fmt.Sprintf("From: %s <%s>\r\n", m.smtpFromName, m.smtpFromEmail))) w.Write([]byte(fmt.Sprintf("To: %s\r\n", p.FirstName+" "+p.LastName+" <"+p.Email+">"))) w.Write([]byte(fmt.Sprintf("Subject: Welcome to RES101\r\n"))) w.Write([]byte("Content-Type: text/plain; charset=utf-8; format=flowed\r\n")) w.Write([]byte("Content-Transfer-Encoding: 8bit\r\n")) w.Write([]byte("\r\n")) w.Write([]byte("Hi " + p.FirstName + ",\r\n")) w.Write([]byte("\r\n")) w.Write([]byte("Welcome to the Planetshakers College Moodle, You now have access to RES101 in\r\n")) w.Write([]byte("Moodle. You can sign-in using the details below:\r\n")) w.Write([]byte("\r\n")) w.Write([]byte(" URL: " + m.base + "\r\n")) w.Write([]byte(" Username: " + p.Email + "\r\n")) w.Write([]byte(" Password: " + pwd + "\r\n")) w.Write([]byte("\r\n")) w.Write([]byte("God bless,\r\n")) w.Write([]byte("Planetshakers College\r\n")) w.Write([]byte("\r\n")) msg := w.Bytes() fmt.Println(string(msg)) var auth smtp.Auth if m.smtpUser != "" && m.smtpPassword != "" { auth = smtp.PlainAuth("", m.smtpUser, m.smtpPassword, m.smtpHost) } // TLS config tlsconfig := &tls.Config{ InsecureSkipVerify: true, ServerName: m.smtpHost, } // Here is the key, you need to call tls.Dial instead of smtp.Dial // for smtp servers running on 465 that require an ssl connection // from the very beginning (no starttls) conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", m.smtpHost, m.smtpPort), tlsconfig) if err != nil { return err } c, err := smtp.NewClient(conn, m.smtpHost) if err != nil { return err } if err = c.Auth(auth); err != nil { return err } if err = c.Mail(m.smtpFromEmail); err != nil { return err } if err = c.Rcpt(p.Email); err != nil { return err } w1, err := c.Data() if err != nil { return err } _, err = w1.Write([]byte(msg)) if err != nil { return err } err = w1.Close() if err != nil { return err } c.Quit() return nil } // Fetch moodle accounts that match match by first and last name. func (m *MoodleApi) GetPeopleByFirstNameLastName(firstname, lastname string) (*[]Person, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&criteria[0][key]=firstname&criteria[0][value]=%s&criteria[0][key]=lastname&criteria[0][value]=%s", m.base, m.token, "core_user_get_users", url.QueryEscape(firstname), url.QueryEscape(lastname)) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return nil, errors.New(message + ". " + url) } type Result struct { Id int64 `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` Email string `json:"email"` Username string `json:"username"` CustomFields []CustomField `json:"customfields"` } type Results struct { People []Result `json:"users"` Total int64 `json:"total"` } var results Results if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } people := make([]Person, 0, len(results.People)) for _, i := range results.People { if strings.ToLower(i.FirstName) == strings.ToLower(firstname) && strings.ToLower(i.LastName) == strings.ToLower(lastname) { people = append(people, Person{MoodleId: i.Id, FirstName: i.FirstName, LastName: i.LastName, Email: i.Email, Username: i.Username}) } } return &people, nil } // Fetch moodle accounts that have a specific field. For example: api.GetPersonByAttribute("firstname", "James") func (m *MoodleApi) GetPeopleByAttribute(attribute, value string) (*[]Person, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&criteria[0][key]=%s&criteria[0][value]=%s", m.base, m.token, "core_user_get_users", url.QueryEscape(attribute), url.QueryEscape(value)) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return nil, errors.New(message + ". " + url) } type Result struct { Id int64 `json:"id"` FirstName string `json:"firstname"` LastName string `json:"lastname"` Email string `json:"email"` Username string `json:"username"` ProfileImageUrl string `json:"profileimageurl,omitempty"` ProfileImageUrlSmall string `json:"profileimageurlsmall,omitempty"` CustomFields []CustomField `json:"customfields"` } type Results struct { People []Result `json:"users"` Total int64 `json:"total"` } var results Results if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } people := make([]Person, 0, len(results.People)) for _, i := range results.People { if strings.Index(i.ProfileImageUrl, "gravatar") > 0 { i.ProfileImageUrl = "" i.ProfileImageUrlSmall = "" } p := Person{MoodleId: i.Id, FirstName: i.FirstName, LastName: i.LastName, Email: i.Email, Username: i.Username, ProfileImageUrl: i.ProfileImageUrl, ProfileImageUrlSmall: i.ProfileImageUrlSmall} for _, c := range i.CustomFields { p.CustomField = append(p.CustomField, CustomField{Name: c.Name, Value: c.Value}) } people = append(people, p) } return &people, nil } // Moodle's bug causes role_id to be ignored: https://tracker.moodle.org/browse/MDL-51152 func (m *MoodleApi) UnsetRole(personId int64, roleId int64, courseId int64) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&enrolments[0][roleid]=%d&enrolments[0][userid]=%d&enrolments[0][courseid]=%d", m.base, m.token, "enrol_manual_unenrol_users", roleId, personId, courseId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } return nil } func (m *MoodleApi) SetRole(personId int64, roleId int64, courseId int64) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&enrolments[0][roleid]=%d&enrolments[0][userid]=%d&enrolments[0][courseid]=%d", m.base, m.token, "enrol_manual_enrol_users", roleId, personId, courseId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } return nil } func (m *MoodleApi) SetUserAttribute(personId int64, attribute, value string) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&users[0][id]=%d&users[0][%s]=%s", m.base, m.token, "core_user_update_users", personId, url.QueryEscape(attribute), url.QueryEscape(value), ) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } if strings.TrimSpace(body) != "" { return errors.New("Server returned unexpected response: " + body) } return nil } // SetAssessmentExtensionDate sets a new due date for an assignment for // a specific user. The userId parameter is the same ID that appears in the // moodle URL when viewing a user. The assessmentId is not the same ID as the // ID shown in a URL when viewing an assessment, it is the ID from the // mdl_assign table. This API updates the mdl_assign_user_flags database // table. func (m *MoodleApi) SetAssessmentExtensionDate(userId, assessmentId int64, newDueDate time.Time) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&assignmentid=%d&userflags[0][userid]=%d&userflags[0][extensionduedate]=%d", m.base, m.token, "mod_assign_set_user_flags", assessmentId, userId, newDueDate.Unix()) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } if strings.HasPrefix(strings.TrimSpace(body), "[{") && strings.Index(body, "\"id\":") > 0 { return nil } return errors.New("Server returned unexpected response: " + body) } func (m *MoodleApi) SetUserCustomField(personId int64, attribute, value string) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&users[0][id]=%d&users[0][customfields][0][type]=%s&users[0][customfields][0][value]=%s", m.base, m.token, "core_user_update_users", personId, url.QueryEscape(attribute), url.QueryEscape(value), ) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } if strings.TrimSpace(body) != "" { return errors.New("Server returned unexpected response: " + body) } return nil } func (m *MoodleApi) RemovePersonFromCourseGroup(personId int64, groupId int64) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&members[0][userid]=%d&members[0][groupid]=%d", m.base, m.token, "core_group_delete_group_members", personId, groupId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } type SiteInfo struct { Sitename string Firstname string Lastname string Userid int64 } if strings.TrimSpace(body) != "null" { return errors.New("Server returned unexpected response: " + body + "--" + url) } return nil } func (m *MoodleApi) AddPersonToCourseGroup(personId int64, groupId int64) error { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&members[0][userid]=%d&members[0][groupid]=%d", m.base, m.token, "core_group_add_group_members", personId, groupId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + url) } type SiteInfo struct { Sitename string Firstname string Lastname string Userid int64 } if strings.TrimSpace(body) != "null" { return errors.New("Server returned unexpected response: " + body + "--" + url) } return nil } func (m *MoodleApi) AddGroupToCourse(courseId int64, groupName, groupDescription string) (int64, error) { if courseId <= 0 { return 0, errors.New("AddGroupToCourse() requires a valid courseId") } if len(strings.TrimSpace(groupName)) == 0 { return 0, errors.New("AddGroupToCourse() requires a valid groupName") } url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&groups[0][courseid]=%d&groups[0][name]=%s&groups[0][description]=%s", m.base, m.token, "core_group_create_groups", courseId, url.QueryEscape(groupName), url.QueryEscape(groupDescription)) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return 0, err } if body == "" { return 0, errors.New("Moodle returned no response") } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return 0, errors.New(message + ". " + url) } type GroupInfo struct { Id int64 Courseid int64 Name string Description string Idnumber string } var response []GroupInfo if err := json.Unmarshal([]byte(body), &response); err != nil { return 0, errors.New("Moodle returned unexpected response. " + err.Error()) } if len(response) != 1 { return 0, errors.New("Moodle returned unexpected response. " + err.Error()) } return response[0].Id, nil } func (m *MoodleApi) AddUser(firstName, lastName, email, username, password string) (int64, error) { if strings.Index(email, "@") < 0 { return 0, errors.New("Invalid email address") } var l string if password == "" { l = fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&users[0][firstname]=%s&users[0][lastname]=%s&users[0][email]=%s&users[0][username]=%s&users[0][createpassword]=1", m.base, m.token, "core_user_create_users", url.QueryEscape(firstName), url.QueryEscape(lastName), url.QueryEscape(email), url.QueryEscape(username)) } else { l = fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&users[0][firstname]=%s&users[0][lastname]=%s&users[0][email]=%s&users[0][username]=%s&users[0][password]=%s", m.base, m.token, "core_user_create_users", url.QueryEscape(firstName), url.QueryEscape(lastName), url.QueryEscape(email), url.QueryEscape(username), url.QueryEscape(password)) } //fmt.Println(l) m.log.Debug("Fetch: %s", l) body, _, _, err := m.fetch.GetUrl(l) fmt.Println(body) if err != nil { return 0, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return 0, errors.New(message + ". " + l) } type SiteInfo struct { Sitename string Firstname string Lastname string Userid int64 } var data []map[string]interface{} if err := json.Unmarshal([]byte(body), &data); err != nil { return 0, errors.New("Server returned unexpected response. " + err.Error()) } if len(data) != 1 { return 0, errors.New("Server returned unexpected response. " + err.Error()) } if _, ok := data[0]["id"]; !ok { return 0, errors.New("Server returned unexpected response. ID is missing. " + err.Error()) } return int64(data[0]["id"].(float64)), nil } // UpdateUser updates the basic details of a moodle account. Requires permission for "core_user_update_users". Password is only updated if password is not blank. func (m *MoodleApi) UpdateUser(id int64, firstName, lastName, email, username, password string) error { if strings.Index(email, "@") < 0 { return errors.New("Invalid email address") } var l string l = fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&users[0][id]=%d&users[0][firstname]=%s&users[0][lastname]=%s&users[0][email]=%s&users[0][username]=%s", m.base, m.token, "core_user_update_users", id, url.QueryEscape(firstName), url.QueryEscape(lastName), url.QueryEscape(email), url.QueryEscape(username)) if password != "" { l = l + "&users[0][password]=" + url.QueryEscape(password) } //fmt.Println(l) m.log.Debug("Fetch: %s", l) body, _, _, err := m.fetch.GetUrl(l) fmt.Println(body) if err != nil { return err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return errors.New(message + ". " + l) } return nil } type CourseGroup struct { Id int64 `json:"id"` Name string `json:"name"` Description string `json:"description"` } type CourseRole struct { Id int64 `json:"id"` Name string `json:"name"` ShortName string `json:"shortname"` } func (m *MoodleApi) GetPersonCourseList(userId int64) ([]Course, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&userid=%d", m.base, m.token, "core_enrol_get_users_courses", userId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return nil, errors.New(message + ". " + url) } var results []Course if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } return results[:], nil } // List the details of each group in a course. Fetches: id, name, and shortname func (m *MoodleApi) GetCourseGroups(courseId int64) ([]CourseGroup, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&courseid=%d", m.base, m.token, "core_group_get_course_groups", courseId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { message := readError(body) return nil, errors.New(message + ". " + url) } var results []CourseGroup if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } return results[:], nil } type CustomField struct { Name string `json:"shortname"` Value string `json:"value"` Type string `json:"type"` } type CoursePerson struct { Id int64 `json:"id"` Username string `json:"username"` FirstName string `json:"firstname"` LastName string `json:"lastname"` Email string `json:"email"` LastAccess int64 `json:"lastaccess"` FirstAccess int64 `json:"firstaccess"` Groups []CourseGroup `json:"groups"` Roles []CourseRole `json:"roles"` CustomFields []CustomField `json:"customfields"` } func (cp *CoursePerson) FirstAccessTime() *time.Time { if cp.FirstAccess == 0 { return nil } t := time.Unix(cp.FirstAccess, 0) return &t } func (cp *CoursePerson) LastAccessTime() *time.Time { if cp.LastAccess == 0 { return nil } t := time.Unix(cp.LastAccess, 0) return &t } func (cp *CoursePerson) CustomField(name string) string { for _, i := range cp.CustomFields { if name == i.Name { return i.Value } } return "" } func (cp *CoursePerson) HasGroupNamed(name string) bool { name = strings.ToLower(name) for _, i := range cp.Groups { if name == strings.ToLower(i.Name) || name == strings.ToLower(i.Description) { return true } } return false } func (cp *CoursePerson) HasRoleNamed(name string) bool { name = strings.ToLower(name) for _, i := range cp.Roles { if name == strings.ToLower(i.Name) || name == strings.ToLower(i.ShortName) { return true } } return false } type GradebookEntry struct { UserId int64 `json:"userid"` Name string `json:"userfullname"` MaxDepth int64 `json:"maxdepth"` Item []GradebookItem `json:"gradeitems"` } type GradebookItem struct { Id int64 `json:"id"` ItemName string `json:"itemname"` ItemType string `json:"itemtype"` ItemModule string `json:"itemmodule"` ItemInstance int64 `json:"iteminstance"` ItemNumber int64 `json:"itemnumber"` CategoryId int64 `json:"categoryid"` OutcomeId int64 `json:"outcomeid"` CmId int64 `json:"cmid"` GradedDate int64 `json:"gradedategraded"` GradeRaw float64 `json:"graderaw"` GradeMax float64 `json:"grademax"` GradeFormatted string `json:"gradeformatted"` GradeDateSubmitted int64 `json:"gradedatesubmitted"` GradeDateGraded int64 `json:"gradedategraded"` PercentageFormatted string `json:"percentageformatted"` WeightRaw float64 `json:"weightraw"` GradeIsHidden bool `json:"gradeishidden"` } func (i *GradebookItem) InferGrade() float64 { if i.GradeMax > 0 && i.GradeRaw > 0 { return i.GradeRaw / i.GradeMax * 100 } if len(i.PercentageFormatted) > 0 && strings.HasSuffix(i.PercentageFormatted, "%") { v := i.PercentageFormatted[0 : len(i.PercentageFormatted)-1] v = strings.TrimSpace(v) r, _ := strconv.ParseFloat(v, 64) return r } return 0 } func (e *GradebookItem) Submitted() *time.Time { if e.GradeDateSubmitted == 0 { return nil } t := time.Unix(e.GradeDateSubmitted, 0) return &t } func (e *GradebookItem) Graded() *time.Time { if e.GradeDateGraded == 0 { return nil } t := time.Unix(e.GradeDateGraded, 0) return &t } // List all gradebook data associated with a course. func (m *MoodleApi) GetCourseGradebook(courseId int64) ([]GradebookEntry, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&courseid=%d", m.base, m.token, "gradereport_user_get_grade_items", courseId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type Results struct { Usergrades []GradebookEntry `json:"usergrades"` } var results Results if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } return results.Usergrades[:], nil } // List all people in a course. Results include the persons roles and groups func (m *MoodleApi) GetCourseRoles(courseId int64) ([]CoursePerson, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&courseid=%d", m.base, m.token, "core_enrol_get_enrolled_users", courseId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } var results []CoursePerson if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } return results[:], nil } func (m *MoodleApi) GetCourses(value string) ([]Course, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&criterianame=search&criteriavalue=%s", m.base, m.token, "core_course_search_courses", url.QueryEscape(value)) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type Result struct { Id int64 `json:"id"` Code string `json:"shortname"` Name string `json:"fullname"` DisplayName string `json:"displayname"` CategoryId int64 `json:"categoryid"` } type Results struct { Courses []Result `json:"courses"` Total int64 `json:"total"` } var results Results if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } subjects := make([]Course, 0, len(results.Courses)) for _, i := range results.Courses { subjects = append(subjects, Course{MoodleId: i.Id, Code: i.Code, Name: i.Name}) } sort.Sort(ByCourseCode(subjects)) return subjects[:], nil } func (m *MoodleApi) GetSiteInfo() (string, string, string, int64, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true", m.base, m.token, "core_webservice_get_site_info") m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return "", "", "", 0, err } if strings.HasPrefix(body, "{\"exception\":\"") { return "", "", "", 0, errors.New(body) } type SiteInfo struct { Sitename string Firstname string Lastname string Userid int64 } var data map[string]interface{} if err := json.Unmarshal([]byte(body), &data); err != nil { return "", "", "", 0, errors.New("Server returned unexpected response. " + err.Error()) } return data["sitename"].(string), data["firstname"].(string), data["lastname"].(string), int64(data["userid"].(float64)), nil } func (r *Restriction) IsRestricted(groups []CourseGroup) bool { switch r.OP { case "&": // Check user is in every group for _, r := range r.C { found := false for _, g := range groups { if r.Id == g.Id { found = true } } if !found { return true } } return false case "!&": // Check user is not in every group for _, r := range r.C { found := false for _, g := range groups { if r.Id == g.Id { found = true } } if found { return true } } return false case "|": // Check user is in one of the groups for _, r := range r.C { for _, g := range groups { if r.Id == g.Id { return false } } } return true case "!|": // Check user is not in one of the groups for _, r := range r.C { for _, g := range groups { if r.Id == g.Id { return true } } } return false default: return false } } type Restriction struct { OP string `json:"op"` C []RestrictionC `json:"c"` Show bool `json:"show"` ShowC []bool `json:"showc"` } type RestrictionC struct { Type string `json:"type"` Id int64 `json:"id"` D string `json:"d"` T int64 `json:"t"` } type CourseModule struct { Id int64 `json:"id"` CourseId int64 `json:"course"` ModuleId int64 `json:"module"` InstanceId int64 `json:"instance"` SectionId int64 `json:"section"` ModuleName string `json:"modname"` Availability Restriction `json:"availability"` Name string `json:"name"` Grade int64 `json:"grade"` Visible bool `json:"visible"` Added *time.Time `json:"added"` } func (m *MoodleApi) GetCourseModule(cmid int64) (*CourseModule, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&cmid=%d", m.base, m.token, "core_course_get_course_module", cmid) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type CourseModuleInt struct { Id int64 `json:"id"` CourseId int64 `json:"course"` ModuleId int64 `json:"module"` InstanceId int64 `json:"instance"` SectionId int64 `json:"section"` ModuleName string `json:"modname"` Name string `json:"name"` Grade int64 `json:"grade"` GradePass string `json:"gradepass"` Availability string `json:"availability"` Added int64 `json:"added"` Visible int64 `json:"visible"` } type Result struct { CM CourseModuleInt `json:"cm"` } var result Result if err := json.Unmarshal([]byte(body), &result); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } var t *time.Time if result.CM.Added != 0 { tt := time.Unix(result.CM.Added, 0) t = &tt } cm := &CourseModule{ Id: result.CM.Id, CourseId: result.CM.CourseId, ModuleId: result.CM.ModuleId, InstanceId: result.CM.InstanceId, SectionId: result.CM.SectionId, ModuleName: result.CM.ModuleName, Name: result.CM.Name, Grade: result.CM.Grade, Visible: result.CM.Visible == 1, Added: t} if result.CM.Availability != "" { if err := json.Unmarshal([]byte(result.CM.Availability), &cm.Availability); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } } return cm, nil } type AssignmentInfo struct { Id int64 `json:"id"` CmId int64 `json:"cmid"` CourseId int64 `json:"courseid"` CourseCode string `json:"coursecode"` CourseName string `json:"coursename"` Name string `json:"name"` NoSubmissions int64 `json:"nosubmissions"` SubmissionDrafts int64 `json:"submissiondrafts"` SendNotifications int64 `json:"sendnotifications"` SendLateNotifications int64 `json:"sendlatenotifications"` SendStudentNotifications int64 `json:"sendstudentnotifications"` Grade int64 `json:"grade"` CompletionSubmit int64 `json:"completionsubmit"` CutoffDate int64 `json:"cutoffdate"` AllowSubmissionsFromDate *time.Time `json:"allowsubmissionsfromdate"` DueDate *time.Time `json:"duedate"` GradingDueDate *time.Time `json:"gradingduedate"` ExtensionDate *time.Time `json:"extensiondate"` } func (m *MoodleApi) GetAssignmentsWithCourseId(courseIds []int) ([]*AssignmentInfo, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&includenotenrolledcourses=1", m.base, m.token, "mod_assign_get_assignments") for i, c := range courseIds { url = fmt.Sprintf("%s&courseids%%5B%d%%5D=%d", url, i, c) } m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type AssignInfo struct { Id int64 `json:"id"` CmId int64 `json:"cmid"` Name string `json:"name"` DueDate int64 `json:"duedate"` } type CourseAssign struct { Id int64 `json:"id"` Code string `json:"shortname"` Name string `json:"fullname"` Assignments []AssignInfo `json:"assignments"` } type Result struct { Courses []CourseAssign `json:"courses"` } var results Result if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } assignments := make([]*AssignmentInfo, 0) for _, c := range results.Courses { for _, a := range c.Assignments { var t *time.Time if a.DueDate != 0 { tt := time.Unix(a.DueDate, 0) t = &tt } ai := &AssignmentInfo{Id: a.Id, CmId: a.CmId, Name: a.Name, CourseCode: c.Code, CourseName: c.Name, CourseId: c.Id, DueDate: t} assignments = append(assignments, ai) } } return assignments[:], nil } type QuizResponse struct { Quizzes []*QuizInfo `json:"quizzes"` //Warnings []ForumDiscussion `json:"warnings"` } type QuizInfo struct { Id int64 `json:"id"` CourseModuleId int64 `json:"coursemodule"` CourseId int64 `json:"course"` Name string `json:"name"` Intro string `json:"intro"` IntroFormat int64 `json:"introformat"` TimeOpen *time.Time `json:"timeopen"` TimeClose *time.Time `json:"timeclose"` TimeLimit int64 `json:"timelimit"` PreferredBehaviour string `json:"preferredbehaviour"` Attempts int64 `json:"attempts"` GradeMethod int64 `json:"grademethod"` DecimalPoints int64 `json:"decimalpoints"` QuestionDecimalPoints int64 `json:"questiondecimalpoints"` ShuffleAnswers int64 `json:"shuffleanswers"` SumGrades int64 `json:"sumgrades"` Grade int64 `json:"grade"` Created *time.Time `json:"timecreated"` Modified *time.Time `json:"timemodified"` Password string `json:"password"` Subnet string `json:"subnet"` HasFeedback int64 `json:"hasfeedback"` Section int64 `json:"section"` Visible int64 `json:"visible"` GroupMode int64 `json:"groupmode"` GroupingId int64 `json:"groupingid"` } func (q *QuizInfo) UnmarshalJSON(data []byte) error { type Alias QuizInfo aux := &struct { TimeOpen int64 `json:"timeopen"` TimeClose int64 `json:"timeclose"` Created int64 `json:"timecreated"` Modified int64 `json:"timemodified"` *Alias }{ Alias: (*Alias)(q), } if err := json.Unmarshal(data, &aux); err != nil { return err } a1 := time.Unix(aux.Created, 0) q.Created = &a1 a2 := time.Unix(aux.Modified, 0) q.Modified = &a2 a3 := time.Unix(aux.TimeOpen, 0) q.TimeOpen = &a3 a4 := time.Unix(aux.TimeClose, 0) q.TimeClose = &a4 return nil } func (m *MoodleApi) GetQuizzesWithCourseId(courseIds []int) ([]*QuizInfo, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true", m.base, m.token, "mod_quiz_get_quizzes_by_courses") for i, c := range courseIds { url = fmt.Sprintf("%s&courseids%%5B%d%%5D=%d", url, i, c) } m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } var results QuizResponse if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } return results.Quizzes[:], nil } type ForumInfo struct { Id int64 `json:"id"` CmId int64 `json:"cmid"` CourseId int64 `json:"courseid"` Scale int64 `json:"scale"` Grade int64 `json:"grade"` GradeForumNotify int64 `json:"grade_forum_notify"` Name string `json:"forum_name"` NumDiscussions int64 `json:"numdiscussions"` Type string `json:"type"` Assessed bool `json:"assessed"` DueDate *time.Time `json:"duedate"` CutoffDate *time.Time `json:"cutoffdate"` } func (m *MoodleApi) GetForumsWithCourseId(courseIds []int) ([]*ForumInfo, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true", m.base, m.token, "mod_forum_get_forums_by_courses") for i, c := range courseIds { url = fmt.Sprintf("%s&courseids%%5B%d%%5D=%d", url, i, c) } m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type ForumResult struct { Id int64 `json:"id"` CourseId int64 `json:"course"` CmId int64 `json:"cmid"` Name string `json:"name"` DueDate int64 `json:"duedate"` CutoffDate int64 `json:"cutoffdate"` GradeForum int64 `json:"grade_forum"` GradeForumNotify int64 `json:"grade_forum_notify"` Assessed int64 `json:"assessed"` Scale int64 `json:"scale"` NumDiscussions int64 `json:"numdiscussions"` Type string `json:"type"` } var results []ForumResult if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } assignments := make([]*ForumInfo, 0) for _, forum := range results { var dueDate *time.Time if forum.DueDate != 0 { tt := time.Unix(forum.DueDate, 0) dueDate = &tt } var cutoffDate *time.Time if forum.CutoffDate != 0 { tt := time.Unix(forum.CutoffDate, 0) cutoffDate = &tt } ai := &ForumInfo{ Id: forum.Id, Scale: forum.Scale, CmId: forum.CmId, Name: forum.Name, CourseId: forum.CourseId, Grade: forum.GradeForum, Assessed: forum.Assessed != 0, Type: forum.Type, NumDiscussions: forum.NumDiscussions, DueDate: dueDate, CutoffDate: cutoffDate, } assignments = append(assignments, ai) } return assignments[:], nil } type ForumDiscussionResponse struct { Discussions []*ForumDiscussion `json:"discussions"` //Warnings []ForumDiscussion `json:"warnings"` } type ForumDiscussion struct { Id int64 `json:"id"` Name string `json:"name"` UserId int64 `json:"userid"` GroupId int64 `json:"groupid"` TimeModified *time.Time `json:"timemodified"` UserModified *time.Time `json:"usermodified"` TimeStart *time.Time `json:"timestart"` TimeEnd *time.Time `json:"timeend"` Discussion int64 `json:"discussion"` Parent int64 `json:"parent"` Created *time.Time `json:"created"` Modified *time.Time `json:"modified"` Mailed int64 `json:"created"` Subject string `json:"subject"` Message string `json:"message"` MessageFormat int64 `json:"messageformat"` MessageTrust int64 `json:"messagetrust"` Attachment bool `json:"attachment"` TotalScore int64 `json:"totalscore"` MailNow int64 `json:"mailnow"` UserFullName string `json:"userfullname"` UserModifiedFullName string `json:"usermodifiedfullname"` UserPictureUrl string `json:"userpictureurl"` UserModifiedPictureUrl string `json:"usermodifiedpictureurl"` NumReplies int64 `json:"numreplies"` NumUnread int64 `json:"numunread"` Pinned bool `json:"pinned"` Locked bool `json:"locked"` Starred bool `json:"starred"` CanReply bool `json:"canreply"` CanLock bool `json:"canlock"` CanFavourite bool `json:"canfavourite"` } func (u *ForumDiscussion) UnmarshalJSON(data []byte) error { type Alias ForumDiscussion aux := &struct { TimeModified int64 `json:"timemodified"` UserModified int64 `json:"usermodified"` TimeStart int64 `json:"timestart"` TimeEnd int64 `json:"timeend"` Created int64 `json:"created"` Modified int64 `json:"modified"` *Alias }{ Alias: (*Alias)(u), } if err := json.Unmarshal(data, &aux); err != nil { return err } a1 := time.Unix(aux.TimeModified, 0) u.TimeModified = &a1 a2 := time.Unix(aux.UserModified, 0) u.UserModified = &a2 a3 := time.Unix(aux.TimeStart, 0) u.TimeStart = &a3 a4 := time.Unix(aux.TimeEnd, 0) u.TimeEnd = &a4 a5 := time.Unix(aux.Created, 0) u.Created = &a5 a6 := time.Unix(aux.Modified, 0) u.Modified = &a6 return nil } func (m *MoodleApi) GetForumsDiscussions(forumId int) ([]*ForumDiscussion, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&forumid=%d", m.base, m.token, "mod_forum_get_forum_discussions", forumId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } var results ForumDiscussionResponse if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } return results.Discussions[:], nil } type AssignmentRecord struct { AssignmentId int64 `json:"assignmentid"` Grades []GradeRecord `json:"grades"` } type GradeRecord struct { Id int64 `json:"id"` UserId int64 `json:"userid"` AttemptNumber int64 `json:"attemptnumber"` TimeCreated int64 `json:"timecreated"` TimeModified int64 `json:"timemodified"` Grader int64 `json:"grade"` Grade float64 `json:"grade"` } func (m *MoodleApi) GetAssignmentGrades(ids ...int64) (*[]AssignmentRecord, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true", m.base, m.token, "mod_assign_get_grades") for i, c := range ids { url = fmt.Sprintf("%s&assignmentids%%5B%d%%5D=%d", url, i, c) } m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type Result struct { Assignments []AssignmentRecord `json:"assignments"` } var results Result if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } return &results.Assignments, nil } type AssignmentSubmission struct { Id int64 `json:"id"` SubmissionId int64 `json:"submissionid"` UserId int64 `json:"userid"` Status string `json:"status"` GradingStatus string `json:"gradingstatus"` Extension *time.Time `json:"extensiondate"` TimeCreated *time.Time `json:"timecreated"` TimeModified *time.Time `json:"timemodified"` } func (m *MoodleApi) GetAssignmentSubmissions(assignmentId int64) ([]*AssignmentSubmission, error) { url := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&assignmentids[0]=%d", m.base, m.token, "mod_assign_get_submissions", assignmentId) m.log.Debug("Fetch: %s", url) body, _, _, err := m.fetch.GetUrl(url) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type Plugin struct { Type string `json:"type"` Name string `json:"name"` } type AssignSub struct { Id int64 `json:"id"` UserId int64 `json:"userid"` Status string `json:"status"` GradingStatus string `json:"gradingstatus"` TimeCreated int64 `json:"timecreated"` TimeModified int64 `json:"timemodified"` Plugins []Plugin `json:"plugins"` } type Assign struct { Id int64 `json:"assignmentid"` Submissions []AssignSub `json:"submissions"` } type Result struct { Assignments []Assign `json:"assignments"` } var results Result if err := json.Unmarshal([]byte(body), &results); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } assignments := make([]*AssignmentSubmission, 0) for _, k := range results.Assignments { for _, i := range k.Submissions { var timeCreated *time.Time var timeModified *time.Time if i.TimeCreated != 0 { tt := time.Unix(i.TimeCreated, 0) timeCreated = &tt } if i.TimeModified != 0 { tt := time.Unix(i.TimeModified, 0) timeModified = &tt } assignments = append(assignments, &AssignmentSubmission{ Id: k.Id, SubmissionId: i.Id, UserId: i.UserId, Status: i.Status, GradingStatus: i.GradingStatus, TimeCreated: timeCreated, TimeModified: timeModified, }) //fmt.Println(i) } } url2 := fmt.Sprintf("%swebservice/rest/server.php?wstoken=%s&wsfunction=%s&moodlewsrestformat=json&moodlewssettingraw=true&assignmentids[0]=%d", m.base, m.token, "mod_assign_get_user_flags", assignmentId) m.log.Debug("Fetch: %s", url2) body, _, _, err = m.fetch.GetUrl(url2) if err != nil { return nil, err } if strings.HasPrefix(body, "{\"exception\":\"") { return nil, errors.New(body) } type Flag struct { Id int64 `json:"id"` UserId int64 `json:"userid"` Extension int64 `json:"extensionduedate"` } type AssignFlag struct { Id int64 `json:"assignmentid"` UserFlags []Flag `json:"userflags"` } type Result2 struct { Assignments []AssignFlag `json:"assignments"` } var results2 Result2 if err := json.Unmarshal([]byte(body), &results2); err != nil { return nil, errors.New("Server returned unexpected response. " + err.Error()) } for _, k := range results2.Assignments { for _, k := range k.UserFlags { // for each extension found, add or append to assignment list if k.Extension == 0 { continue } var t *time.Time tt := time.Unix(k.Extension, 0) t = &tt found := false for _, a := range assignments { if a.UserId == k.UserId && k.Extension > 0 { a.Extension = t } } if !found { assignments = append(assignments, &AssignmentSubmission{UserId: k.UserId, Status: "new", GradingStatus: "", Extension: t}) } } } return assignments[:], nil } func GetAttendance() error { // Get attendance for a session // But how to we know which sessions to look at? return nil } func (m *MoodleApi) SetUrlFetcher(fetch LookupUrl) { m.fetch = fetch }