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