feat: usos sign-in, private calendar feed and timetable archive

This commit is contained in:
hh committed 2026-10-11 00:47:38 +00:00
commit 7d4e21f977
33 files changed
+3270

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
package ical
import (
"io"
"strings"
"time"
"unicode/utf8"
"usoscal/internal/store"
)
const (
stamp = "20060102T150405Z"
lineLimit = 75
)
func Summary(event store.Event) string {
if event.Kind == "" || event.Kind == event.Title {
return event.Title
}
return event.Title + " (" + event.Kind + ")"
}
func Location(event store.Event) string {
place := event.Room
switch {
case place != "" && event.Building != "":
place += " (" + event.Building + ")"
case place == "":
place = event.Building
}
return joinNonEmpty(", ", place, event.Address)
}
func Description(event store.Event) string {
about := event.Kind
if event.Group != "" {
about = joinNonEmpty(", ", about, "group "+event.Group)
}
lecturers := ""
if event.Lecturers != "" {
lecturers = "Lecturer: " + event.Lecturers
}
return joinNonEmpty("\n", about, lecturers, event.URL)
}
func Write(w io.Writer, name, host string, events []store.Event) error {
var b strings.Builder
line(&b, "BEGIN:VCALENDAR")
line(&b, "VERSION:2.0")
line(&b, "PRODID:-//usoscal//EN")
line(&b, "CALSCALE:GREGORIAN")
line(&b, "METHOD:PUBLISH")
line(&b, "X-WR-CALNAME:"+escape(name))
line(&b, "X-WR-TIMEZONE:Europe/Warsaw")
line(&b, "REFRESH-INTERVAL;VALUE=DURATION:PT6H")
line(&b, "X-PUBLISHED-TTL:PT6H")
for _, event := range events {
line(&b, "BEGIN:VEVENT")
line(&b, "UID:"+event.UID+"@"+host)
line(&b, "DTSTAMP:"+utc(event.UpdatedAt))
line(&b, "LAST-MODIFIED:"+utc(event.UpdatedAt))
line(&b, "DTSTART:"+utc(event.Start))
line(&b, "DTEND:"+utc(event.End))
line(&b, "SUMMARY:"+escape(Summary(event)))
if location := Location(event); location != "" {
line(&b, "LOCATION:"+escape(location))
}
if description := Description(event); description != "" {
line(&b, "DESCRIPTION:"+escape(description))
}
if event.URL != "" {
line(&b, "URL:"+event.URL)
}
line(&b, "END:VEVENT")
}
line(&b, "END:VCALENDAR")
_, err := io.WriteString(w, b.String())
return err
}
func utc(t time.Time) string {
return t.UTC().Format(stamp)
}
func joinNonEmpty(separator string, parts ...string) string {
kept := parts[:0:0]
for _, part := range parts {
if part != "" {
kept = append(kept, part)
}
}
return strings.Join(kept, separator)
}
var escaper = strings.NewReplacer(`\`, `\\`, ";", `\;`, ",", `\,`, "\r\n", `\n`, "\n", `\n`, "\r", "")
func escape(text string) string {
return escaper.Replace(text)
}
func line(b *strings.Builder, content string) {
for len(content) > lineLimit {
cut := lineLimit
for cut > 0 && !utf8.RuneStart(content[cut]) {
cut--
}
b.WriteString(content[:cut])
b.WriteString("\r\n ")
content = content[cut:]
}
b.WriteString(content)
b.WriteString("\r\n")
}
+76
View File
@@ -0,0 +1,76 @@
package ical
import (
"strings"
"testing"
"time"
"unicode/utf8"
"usoscal/internal/store"
)
func TestWrite(t *testing.T) {
warsaw, err := time.LoadLocation("Europe/Warsaw")
if err != nil {
t.Fatal(err)
}
event := store.Event{
UID: "sm-90917229",
Start: time.Date(2026, 10, 9, 11, 45, 0, 0, warsaw),
End: time.Date(2026, 10, 9, 13, 15, 0, 0, warsaw),
Title: "Algebra liniowa z elementami matematyki dyskretnej",
Kind: "Laboratorium",
Group: "2",
Room: "602",
Building: "A1 - BM - Budowa Maszyn, budynek z zegarem",
Address: "ul. Piotrowo 3, 61-138 Poznań, Polska",
Lecturers: "dr inż. Zażółć Gęślą-Jaźń",
URL: "https://usosweb.put.poznan.pl/kontroler.php?_action=x&gr_nr=2",
UpdatedAt: time.Date(2026, 10, 1, 8, 0, 0, 0, time.UTC),
}
var out strings.Builder
if err := Write(&out, "USOS", "usos.example", []store.Event{event}); err != nil {
t.Fatal(err)
}
text := out.String()
for _, rawLine := range strings.Split(strings.TrimSuffix(text, "\r\n"), "\r\n") {
if len(rawLine) > lineLimit+1 {
t.Errorf("line longer than the fold limit: %q", rawLine)
}
if !utf8.ValidString(rawLine) {
t.Errorf("fold split a character: %q", rawLine)
}
}
if strings.Contains(strings.ReplaceAll(text, "\r\n", ""), "\n") {
t.Error("bare line feed in the output")
}
unfolded := strings.ReplaceAll(text, "\r\n ", "")
for _, want := range []string{
"UID:sm-90917229@usos.example\r\n",
"DTSTART:20261009T094500Z\r\n",
"DTEND:20261009T111500Z\r\n",
"SUMMARY:Algebra liniowa z elementami matematyki dyskretnej (Laboratorium)\r\n",
`LOCATION:602 (A1 - BM - Budowa Maszyn\, budynek z zegarem)\, ul. Piotrowo 3\, 61-138 Poznań\, Polska` + "\r\n",
`DESCRIPTION:Laboratorium\, group 2\nLecturer: dr inż. Zażółć Gęślą-Jaźń\nhttps://usosweb.put.poznan.pl/kontroler.php?_action=x&gr_nr=2` + "\r\n",
"END:VCALENDAR\r\n",
} {
if !strings.Contains(unfolded, want) {
t.Errorf("missing %q in\n%s", want, unfolded)
}
}
}
func TestSparseEvent(t *testing.T) {
event := store.Event{UID: "x", Title: "Spotkanie", Kind: "Spotkanie", Start: time.Unix(0, 0), End: time.Unix(60, 0)}
if got := Summary(event); got != "Spotkanie" {
t.Errorf("Summary = %q", got)
}
if got := Location(event); got != "" {
t.Errorf("Location = %q", got)
}
if got := Description(event); got != "Spotkanie" {
t.Errorf("Description = %q", got)
}
}