Initial commit

This commit is contained in:
Nicholas Novak 2023-03-17 18:23:40 -07:00
commit 51b7e0569c
8 changed files with 268 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.toml

15
README.md Normal file
View 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
View 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
View 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
View 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
View 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),
)
}
}

View File

@ -0,0 +1,7 @@
package selection
type CalendarEventType string
const (
AllEvents CalendarEventType = "all"
)

7
timeline/timelines.go Normal file
View File

@ -0,0 +1,7 @@
package timeline
type CalendarEventHorizon string
const (
WithinMonth CalendarEventHorizon = "monthnow"
)