feat: add calendar

master
Evan Chen 2021-12-24 16:56:16 +08:00
parent 82c82b0b42
commit 8f207bf356
11 changed files with 505 additions and 3 deletions

117
calendar/api_cal.go Normal file
View File

@ -0,0 +1,117 @@
package calendar
import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/errors"
"kumoly.io/kumoly/app/history"
"kumoly.io/kumoly/app/server"
)
func ApiCalQuery(c *gin.Context) {
id := c.Query("id")
if id != "" {
cal := &Calendar{}
err := HasCalAccess(c, cal, id)
if err != nil {
panic(err)
}
server.OK(c, cal)
} else {
grp := c.Query("grp")
cals := []Calendar{}
cl, err := auth.GetContextClaims(c)
if err != nil {
panic(err)
}
var result *gorm.DB
if grp != "" && auth.ACHas(c, auth.ADMIN, auth.SYSTEM, grp) {
var grp_id uint
db.Raw("select id from groups where name = ?", grp).Scan(&grp_id)
if grp_id == 0 {
panic(errors.ErrorNotFound)
}
result = db.Find(&cals, "`group_id` = ? ", grp_id)
} else if !auth.ACHas(c, auth.ADMIN, auth.SYSTEM) {
result = db.
Find(&cals, "`group_id` in (?) or group_id = 0",
db.Table("groups").Select("id").Where("name in ?", cl.Groups))
} else {
result = db.Find(&cals)
}
if result.Error != nil {
panic(result.Error)
}
server.OK(c, cals)
}
}
func ApiCalNew(c *gin.Context) {
cal := &Calendar{}
if err := c.ShouldBindJSON(cal); err != nil {
panic(err)
}
if cal.ID != "" {
panic(errors.ErrorBadRequest)
}
if !auth.ACHas(c, auth.ADMIN, auth.SYSTEM, cal.GroupName) {
panic(errors.ErrorForbidden)
}
if err := db.Create(cal).Error; err != nil {
panic(err)
}
history.Send(history.Info().
Nm("Create").
Grp(cal.GroupName).Bd(cal).
Iss(c.GetString(auth.GinUserKey)).
Msg("Calendar created"))
server.OK(c, cal)
}
func ApiCalUpdate(c *gin.Context) {
cal := &Calendar{}
if err := c.ShouldBindJSON(cal); err != nil {
panic(err)
}
if cal.ID == "" {
panic(errors.ErrorBadRequest)
}
if err := HasCalAccess(c, &Calendar{}, cal.ID); err != nil {
panic(errors.ErrorForbidden)
}
if err := db.Save(cal).Error; err != nil {
panic(err)
}
history.Send(history.Info().
Nm("Update").
Grp(cal.GroupName).Bd(cal).
Iss(c.GetString(auth.GinUserKey)).
Msg("Calendar Updated"))
server.OK(c, cal)
}
func ApiCalDelete(c *gin.Context) {
id := c.Query("id")
if id == "" {
panic(errors.ErrorBadRequest)
}
cal := &Calendar{}
err := HasCalAccess(c, cal, id)
if err != nil {
panic(err)
}
err = db.Delete(&Calendar{}, "id = ?", id).Error
if err != nil {
panic(errors.NewError(404, err))
}
history.Send(history.Info().
Nm("Delete").
Grp(cal.GroupName).Bd(cal).
Iss(c.GetString(auth.GinUserKey)).
Msg("Calendar Deleted"))
server.OK(c, "ok")
}

120
calendar/api_event.go Normal file
View File

@ -0,0 +1,120 @@
package calendar
import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/errors"
"kumoly.io/kumoly/app/history"
"kumoly.io/kumoly/app/server"
)
func ApiEventQuery(c *gin.Context) {
id := c.Query("id")
if id != "" {
e := &Event{}
err := HasEventAccess(c, e, id)
if err != nil {
panic(err)
}
server.OK(c, e)
} else {
grp := c.Query("grp")
events := []Event{}
cl, err := auth.GetContextClaims(c)
if err != nil {
panic(err)
}
var result *gorm.DB
if grp != "" && auth.ACHas(c, auth.ADMIN, auth.SYSTEM, grp) {
var grp_id uint
db.Raw("select id from groups where name = ?", grp).Scan(&grp_id)
if grp_id == 0 {
panic(errors.ErrorNotFound)
}
result = db.Find(&events, "`group_id` = ? ", grp_id)
} else if !auth.ACHas(c, auth.ADMIN, auth.SYSTEM) {
result = db.
Find(&events, "`group_id` in (?) or group_id = 0",
db.Table("groups").Select("id").Where("name in ?", cl.Groups))
} else {
result = db.Find(&events)
}
if result.Error != nil {
panic(result.Error)
}
server.OK(c, events)
}
}
func ApiEventNew(c *gin.Context) {
e := &Event{}
if err := c.ShouldBindJSON(e); err != nil {
panic(err)
}
if e.ID != "" {
panic(errors.ErrorBadRequest)
}
if e.Start.IsZero() || e.End.IsZero() || e.Start.Before(e.End) {
panic(ErrorInvalidTime)
}
if !auth.ACHas(c, auth.ADMIN, auth.SYSTEM, e.GroupName) {
panic(errors.ErrorForbidden)
}
if err := db.Create(e).Error; err != nil {
panic(err)
}
history.Send(history.Info().
Nm("Create").
Grp(e.GroupName).Bd(e).
Iss(c.GetString(auth.GinUserKey)).
Msg("Event created"))
server.OK(c, e)
}
func ApiEventUpdate(c *gin.Context) {
e := &Event{}
if err := c.ShouldBindJSON(e); err != nil {
panic(err)
}
if e.ID == "" {
panic(errors.ErrorBadRequest)
}
if err := HasEventAccess(c, &Event{}, e.ID); err != nil {
panic(errors.ErrorForbidden)
}
if err := db.Save(e).Error; err != nil {
panic(err)
}
history.Send(history.Info().
Nm("Update").
Grp(e.GroupName).Bd(e).
Iss(c.GetString(auth.GinUserKey)).
Msg("Event Updated"))
server.OK(c, e)
}
func ApiEventDelete(c *gin.Context) {
id := c.Query("id")
if id == "" {
panic(errors.ErrorBadRequest)
}
e := &Event{}
err := HasEventAccess(c, e, id)
if err != nil {
panic(err)
}
err = db.Delete(&Calendar{}, "id = ?", id).Error
if err != nil {
panic(errors.NewError(404, err))
}
history.Send(history.Info().
Nm("Delete").
Grp(e.GroupName).Bd(e).
Iss(c.GetString(auth.GinUserKey)).
Msg("Event Deleted"))
server.OK(c, "ok")
}

View File

@ -6,22 +6,62 @@ import (
"github.com/rs/xid" "github.com/rs/xid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"gorm.io/gorm" "gorm.io/gorm"
"kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/errors"
) )
var l zerolog.Logger var l zerolog.Logger
var db *gorm.DB
type Calendar struct { type Calendar struct {
ID string `gorm:"primaryKey"` ID string `gorm:"primaryKey"`
Name string `gorm:"index:idx_cal,unique"`
Description string
ExtLink string
Timezone string
Events []Event Events []Event
Group auth.Group `json:"-"`
GroupName string `gorm:"-" json:"Group"`
GroupID uint `gorm:"index:idx_cal,unique" json:"-"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
func (c *Calendar) BeforeSave(tx *gorm.DB) (err error) {
if c.GroupName != "" {
var grp_id uint
db.Raw("select id from groups where name = ?", c.GroupName).Scan(&grp_id)
if grp_id == 0 {
return errors.ErrorNotFound
}
c.GroupID = grp_id
} else {
c.GroupID = 0
}
return
}
func (c *Calendar) BeforeCreate(tx *gorm.DB) (err error) { func (c *Calendar) BeforeCreate(tx *gorm.DB) (err error) {
if c.ID == "" { if c.ID == "" {
c.ID = xid.New().String() c.ID = xid.New().String()
} }
if c.Timezone == "" {
c.Timezone = "Asia/Taipei"
}
return
}
func (c *Calendar) AfterFind(tx *gorm.DB) (err error) {
if c.GroupID != 0 {
var name string
db.Raw("select name from groups where id = ?", c.GroupID).Scan(&name)
c.GroupName = name
}
return return
} }

16
calendar/calendar_test.go Normal file
View File

@ -0,0 +1,16 @@
package calendar
import (
"fmt"
"testing"
"time"
)
func TestOffsetStr(t *testing.T) {
fmt.Println(offsetStr(-28800))
fmt.Println(offsetStr(28800))
}
func TestT(t *testing.T) {
fmt.Println(icstime(time.Now()))
}

13
calendar/errors.go Normal file
View File

@ -0,0 +1,13 @@
package calendar
import (
"net/http"
"kumoly.io/kumoly/app/errors"
)
var ErrorInvalidTime = errors.Error{
Code: http.StatusBadRequest,
ID: "ErrorInvalidTime",
Message: "time is not valid",
}

View File

@ -5,24 +5,62 @@ import (
"github.com/rs/xid" "github.com/rs/xid"
"gorm.io/gorm" "gorm.io/gorm"
"kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/errors"
) )
type Event struct { type Event struct {
ID string `gorm:"primaryKey"` ID string `gorm:"primaryKey"`
Start time.Time Start time.Time
End time.Time End time.Time
Name string `gorm:"not null"`
Hosts string
Description string
Location string
// ntu
Class string
CourseID string
Semester string
CalendarID string CalendarID string
EventGroupID string EventGroupID string
Group auth.Group `json:"-"`
GroupName string `gorm:"-" json:"Group"`
GroupID uint `json:"-"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
func (e *Event) BeforeSave(tx *gorm.DB) (err error) {
if e.GroupName != "" {
var grp_id uint
db.Raw("select id from groups where name = ?", e.GroupName).Scan(&grp_id)
if grp_id == 0 {
return errors.ErrorNotFound
}
e.GroupID = grp_id
} else {
e.GroupID = 0
}
return
}
func (e *Event) BeforeCreate(tx *gorm.DB) (err error) { func (e *Event) BeforeCreate(tx *gorm.DB) (err error) {
if e.ID == "" { if e.ID == "" {
e.ID = xid.New().String() e.ID = xid.New().String()
} }
return return
} }
func (e *Event) AfterFind(tx *gorm.DB) (err error) {
if e.GroupID != 0 {
var name string
db.Raw("select name from groups where id = ?", e.GroupID).Scan(&name)
e.GroupName = name
}
return
}

View File

@ -5,20 +5,51 @@ import (
"github.com/rs/xid" "github.com/rs/xid"
"gorm.io/gorm" "gorm.io/gorm"
"kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/errors"
) )
type EventGroup struct { type EventGroup struct {
ID string `gorm:"primaryKey"` ID string `gorm:"primaryKey"`
Name string
Rrule string
Events []Event Events []Event
Group auth.Group `json:"-"`
GroupName string `gorm:"-" json:"Group"`
GroupID uint `json:"-"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
func (eg *EventGroup) BeforeSave(tx *gorm.DB) (err error) {
if eg.GroupName != "" {
var grp_id uint
db.Raw("select id from groups where name = ?", eg.GroupName).Scan(&grp_id)
if grp_id == 0 {
return errors.ErrorNotFound
}
eg.GroupID = grp_id
} else {
eg.GroupID = 0
}
return
}
func (eg *EventGroup) BeforeCreate(tx *gorm.DB) (err error) { func (eg *EventGroup) BeforeCreate(tx *gorm.DB) (err error) {
if eg.ID == "" { if eg.ID == "" {
eg.ID = xid.New().String() eg.ID = xid.New().String()
} }
return return
} }
func (eg *EventGroup) AfterFind(tx *gorm.DB) (err error) {
if eg.GroupID != 0 {
var name string
db.Raw("select name from groups where id = ?", eg.GroupID).Scan(&name)
eg.GroupName = name
}
return
}

74
calendar/ics.go Normal file
View File

@ -0,0 +1,74 @@
package calendar
import (
"bytes"
"fmt"
"text/template"
"time"
"kumoly.io/kumoly/app/store"
)
const ics_base = `BEGIN:VCALENDAR
PRODID:-//Kumoly//Kumoly App v0.0.1//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:{{.Name}}
X-WR-TIMEZONE:Asia/Taipei
X-WR-CALDESC:{{.Name}}
BEGIN:VTIMEZONE
TZID:{{.Timezone}}
X-LIC-LOCATION:{{.Timezone}}
BEGIN:STANDARD
TZOFFSETFROM:+0800
TZOFFSETTO:+0800
TZNAME:CST
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
{{with .Events}}{{ if gt (len .) 0 }}{{ range $event := . }}
BEGIN:VEVENT
DTSTART:{{$event.Start|icstime}}
DTEND:{{$event.End|icstime}}
DTSTAMP:{{now|icstime}}
UID:{{$event.ID}}
CREATED:{{$event.CreatedAt|icstime}}
DESCRIPTION:{{$event.ID}}
LAST-MODIFIED:{{$event.UpdatedAt|icstime}}
LOCATION:{{$event.Location}}
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:{{$event.Name}}
TRANSP:TRANSPARENT
END:VEVENT
{{end}}{{end}}{{end}}
END:VCALENDAR`
var tmpl *template.Template
func init() {
tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"icstime": icstime,
"now": func() time.Time { return time.Now() },
}).Parse(ics_base))
}
func ICS(cal_id string, from time.Time, to time.Time) (string, error) {
cal := &Calendar{}
store.DB.Preload("Events", "start > ? and end < ?", from, to).
First(cal, "id = ?", cal_id)
buf := &bytes.Buffer{}
if err := tmpl.Execute(buf, cal); err != nil {
return "", err
}
return buf.String(), nil
}
func icstime(t time.Time) string {
return t.UTC().Format("20060102T150405Z")
}
func offsetStr(offset int) string {
return fmt.Sprintf("%+05d", offset/36)
}

View File

@ -1,6 +1,7 @@
package calendar package calendar
import ( import (
"kumoly.io/kumoly/app/server"
"kumoly.io/kumoly/app/store" "kumoly.io/kumoly/app/store"
"kumoly.io/kumoly/app/util" "kumoly.io/kumoly/app/util"
) )
@ -15,6 +16,7 @@ func (srv Service) Del() {}
func (srv Service) Health() error { return nil } func (srv Service) Health() error { return nil }
func (srv Service) Init() error { func (srv Service) Init() error {
db = store.DB
l = util.Klog.With().Str("mod", "auth").Logger() l = util.Klog.With().Str("mod", "auth").Logger()
l.Debug().Msg("Migrating database for calendar.Service ...") l.Debug().Msg("Migrating database for calendar.Service ...")
@ -25,4 +27,18 @@ func (srv Service) Init() error {
return nil return nil
} }
func (srv Service) Load() error { return nil } func (srv Service) Load() error {
calApi := server.API.Group("cal")
calApi.GET("", ApiCalQuery)
calApi.POST("", ApiCalNew)
calApi.PUT("", ApiCalUpdate)
calApi.DELETE("", ApiCalDelete)
eventApi := server.API.Group("event")
eventApi.GET("", ApiEventQuery)
eventApi.POST("", ApiEventNew)
eventApi.PUT("", ApiEventUpdate)
eventApi.DELETE("", ApiEventDelete)
return nil
}

35
calendar/util.go Normal file
View File

@ -0,0 +1,35 @@
package calendar
import (
"github.com/gin-gonic/gin"
"kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/errors"
)
func HasCalAccess(c *gin.Context, cal *Calendar, cid string) error {
err := db.First(cal, "id = ?", cid).Error
if err != nil {
return errors.NewError(404, err)
}
if cal.GroupName == "" {
return nil
}
if !auth.ACHas(c, auth.ADMIN, auth.SYSTEM, cal.GroupName) {
return errors.ErrorForbidden
}
return nil
}
func HasEventAccess(c *gin.Context, e *Event, cid string) error {
err := db.First(e, "id = ?", cid).Error
if err != nil {
return errors.NewError(404, err)
}
if e.GroupName == "" {
return nil
}
if !auth.ACHas(c, auth.ADMIN, auth.SYSTEM, e.GroupName) {
return errors.ErrorForbidden
}
return nil
}

View File

@ -4,6 +4,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"kumoly.io/kumoly/app/attribute" "kumoly.io/kumoly/app/attribute"
"kumoly.io/kumoly/app/auth" "kumoly.io/kumoly/app/auth"
"kumoly.io/kumoly/app/calendar"
"kumoly.io/kumoly/app/control" "kumoly.io/kumoly/app/control"
"kumoly.io/kumoly/app/email" "kumoly.io/kumoly/app/email"
"kumoly.io/kumoly/app/history" "kumoly.io/kumoly/app/history"
@ -30,6 +31,7 @@ func Default() *system.System {
&history.Service{}, &history.Service{},
&email.Service{}, &email.Service{},
&control.Service{}, &control.Service{},
&calendar.Service{},
) )
return sys return sys
} }