Initial commit
This commit is contained in:
commit
51b7e0569c
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.toml
|
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# assign-notify
|
||||||
|
|
||||||
|
A small script that displays the upcoming assignments on my school's Moodle site
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Everything in `config.toml`
|
||||||
|
|
||||||
|
An example looks like:
|
||||||
|
```toml
|
||||||
|
moodle_root = "https://moodle.example.com"
|
||||||
|
user_id = "12345"
|
||||||
|
auth_token = "aaaabbbbccccddddeeeeffff"
|
||||||
|
```
|
||||||
|
These values can be obtained by exporting a calendar in Moodle, and looking at the resulting link that was generated
|
111
calendar/calendarevent.go
Normal file
111
calendar/calendarevent.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package calendar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ics "github.com/arran4/golang-ical"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CalendarClass is a class that represents what "class" the specified event is
|
||||||
|
//
|
||||||
|
// Currently, I have only observed this value as being `PUBLIC`, but other
|
||||||
|
// values are handled as panics
|
||||||
|
type CalendarClass byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClassPublic CalendarClass = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseClass(raw string) CalendarClass {
|
||||||
|
switch raw {
|
||||||
|
case "PUBLIC":
|
||||||
|
return ClassPublic
|
||||||
|
}
|
||||||
|
panic("Unknown class: " + raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc CalendarClass) String() string {
|
||||||
|
switch cc {
|
||||||
|
case ClassPublic:
|
||||||
|
return "PUBLIC"
|
||||||
|
default:
|
||||||
|
panic("Unknown class")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalendarEvent represents a single event in a calendar that is tied to a certain date
|
||||||
|
type CalendarEvent struct {
|
||||||
|
UID string
|
||||||
|
// A summary of the event, including the name of the event
|
||||||
|
// This value is set every time as far as I can tell
|
||||||
|
Summary string
|
||||||
|
// A fescription of the event, including more specific information. Not always
|
||||||
|
// set, so don't rely on it
|
||||||
|
Description string
|
||||||
|
// Access class? of the event
|
||||||
|
Class CalendarClass
|
||||||
|
LastModified time.Time
|
||||||
|
DateStamp time.Time
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
// Every event comes with a category. This usually contains the name of the
|
||||||
|
// class, along with some other information (Semester code, CRN, sections)
|
||||||
|
Category struct {
|
||||||
|
ClassName string
|
||||||
|
OtherData string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarTimeFormat = "20060102T150405Z"
|
||||||
|
|
||||||
|
// FromVEvent parses an ICS VEvent's format and outputs a specific calendar event, or an error
|
||||||
|
func FromVEvent(external *ics.VEvent) (event CalendarEvent, err error) {
|
||||||
|
if len(external.Components) > 0 {
|
||||||
|
panic("Event has unhandled components")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prop := range external.Properties {
|
||||||
|
if len(prop.ICalParameters) > 0 {
|
||||||
|
panic("Property has unhandled ICalParameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch prop.IANAToken {
|
||||||
|
case "UID":
|
||||||
|
event.UID = prop.Value
|
||||||
|
case "SUMMARY":
|
||||||
|
event.Summary = prop.Value
|
||||||
|
case "DESCRIPTION":
|
||||||
|
event.Description = prop.Value
|
||||||
|
case "CLASS":
|
||||||
|
event.Class = ParseClass(prop.Value)
|
||||||
|
case "LAST-MODIFIED":
|
||||||
|
event.LastModified, err = time.Parse(calendarTimeFormat, prop.Value)
|
||||||
|
if err != nil {
|
||||||
|
return event, err
|
||||||
|
}
|
||||||
|
case "DTSTAMP":
|
||||||
|
event.DateStamp, err = time.Parse(calendarTimeFormat, prop.Value)
|
||||||
|
if err != nil {
|
||||||
|
return event, err
|
||||||
|
}
|
||||||
|
case "DTSTART":
|
||||||
|
event.StartTime, err = time.Parse(calendarTimeFormat, prop.Value)
|
||||||
|
if err != nil {
|
||||||
|
return event, err
|
||||||
|
}
|
||||||
|
case "DTEND":
|
||||||
|
event.EndTime, err = time.Parse(calendarTimeFormat, prop.Value)
|
||||||
|
if err != nil {
|
||||||
|
return event, err
|
||||||
|
}
|
||||||
|
case "CATEGORIES":
|
||||||
|
dashIndex := strings.Index(prop.Value, "-")
|
||||||
|
event.Category.ClassName = prop.Value[:dashIndex]
|
||||||
|
event.Category.OtherData = prop.Value[dashIndex:]
|
||||||
|
default:
|
||||||
|
panic("Unknown token when parsing event: " + prop.IANAToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return event, nil
|
||||||
|
}
|
7
go.mod
Normal file
7
go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module github.com/NickyBoy89/assign-notify
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0
|
||||||
|
|
||||||
|
require github.com/BurntSushi/toml v1.2.1
|
22
go.sum
Normal file
22
go.sum
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0 h1:VVPogIxPiZ6WK5G4Pve5VSQ4HEFiJ8GChpqRjo1gN2c=
|
||||||
|
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
98
main.go
Normal file
98
main.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/NickyBoy89/assign-notify/calendar"
|
||||||
|
"github.com/NickyBoy89/assign-notify/selection"
|
||||||
|
"github.com/NickyBoy89/assign-notify/timeline"
|
||||||
|
ics "github.com/arran4/golang-ical"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConstructCalendarUrl(
|
||||||
|
moodleBase, userId, authToken string,
|
||||||
|
eventTypes selection.CalendarEventType,
|
||||||
|
when timeline.CalendarEventHorizon,
|
||||||
|
) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s/calendar/export_execute.php?userid=%s&authtoken=%s&preset_what=%s&preset_time=%s",
|
||||||
|
moodleBase,
|
||||||
|
userId,
|
||||||
|
authToken,
|
||||||
|
eventTypes,
|
||||||
|
when,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigFileName = "config.toml"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
MoodleRoot string `toml:"moodle_root"`
|
||||||
|
UserId string `toml:"user_id"`
|
||||||
|
AuthToken string `toml:"auth_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Load the config
|
||||||
|
var conf Config
|
||||||
|
confFile, err := os.Open(ConfigFileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to open config %s: %s", ConfigFileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := toml.NewDecoder(confFile).Decode(&conf); err != nil {
|
||||||
|
log.Fatalf("Error decoding %s: %s", ConfigFileName, err)
|
||||||
|
}
|
||||||
|
confFile.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(ConstructCalendarUrl(
|
||||||
|
conf.MoodleRoot,
|
||||||
|
conf.UserId,
|
||||||
|
conf.AuthToken,
|
||||||
|
selection.AllEvents,
|
||||||
|
timeline.WithinMonth,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error requesting calendar: %s", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
cal, err := ics.ParseCalendar(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error parsing ICAL: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the events into when their due dates are
|
||||||
|
var past, upcoming []calendar.CalendarEvent
|
||||||
|
|
||||||
|
for _, event := range cal.Events() {
|
||||||
|
calEvent, err := calendar.FromVEvent(event)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if calEvent.EndTime.Before(time.Now()) {
|
||||||
|
past = append(past, calEvent)
|
||||||
|
} else {
|
||||||
|
upcoming = append(upcoming, calEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range upcoming {
|
||||||
|
due := time.Until(event.EndTime)
|
||||||
|
days := int(due.Hours()) / 24
|
||||||
|
hours := int(due.Hours())
|
||||||
|
fmt.Printf(
|
||||||
|
"%s: %s: due in %v days %v hours %.0f minutes\n",
|
||||||
|
event.Category.ClassName,
|
||||||
|
event.Summary,
|
||||||
|
days,
|
||||||
|
hours-(days*24),
|
||||||
|
due.Minutes()-float64(hours*60),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
7
selection/eventselection.go
Normal file
7
selection/eventselection.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package selection
|
||||||
|
|
||||||
|
type CalendarEventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AllEvents CalendarEventType = "all"
|
||||||
|
)
|
7
timeline/timelines.go
Normal file
7
timeline/timelines.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package timeline
|
||||||
|
|
||||||
|
type CalendarEventHorizon string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WithinMonth CalendarEventHorizon = "monthnow"
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user