The gofakeit upgrade uses the new math/rand/v2 package, which uses uint64 more than int64, so we had to change a bunch of row IDs from int64 to uint64.
161 lines
3.7 KiB
Go
161 lines
3.7 KiB
Go
package calendar
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/apognu/gocal"
|
|
"github.com/timelinize/timelinize/timeline"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func init() {
|
|
err := timeline.RegisterDataSource(timeline.DataSource{
|
|
Name: "calendar",
|
|
Title: "Calendar",
|
|
Icon: "calendar.svg",
|
|
NewOptions: func() any { return new(Options) },
|
|
NewFileImporter: func() timeline.FileImporter { return new(FileImporter) },
|
|
})
|
|
if err != nil {
|
|
timeline.Log.Fatal("registering data source", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// FileImporter can import the data from a file.
|
|
type FileImporter struct{}
|
|
|
|
// Recognize returns whether the file is recognized for this data source.
|
|
func (fi FileImporter) Recognize(_ context.Context, dirEntry timeline.DirEntry, _ timeline.RecognizeParams) (timeline.Recognition, error) {
|
|
rec := timeline.Recognition{DirThreshold: 0.9}
|
|
|
|
// TODO: proper detection, not just filename
|
|
ext := strings.ToLower(path.Ext(dirEntry.Name()))
|
|
if ext == extIcs {
|
|
rec.Confidence = 1
|
|
}
|
|
|
|
return rec, nil
|
|
}
|
|
|
|
// Options configures the data source.
|
|
type Options struct {
|
|
// The ID of the owner entity. REQUIRED for linking
|
|
// entity in DB when calendar provides no/insufficient
|
|
// owner information.
|
|
OwnerEntityID uint64 `json:"owner_entity_id"`
|
|
}
|
|
|
|
// FileImport imports data from a file.
|
|
func (fi FileImporter) FileImport(ctx context.Context, dirEntry timeline.DirEntry, params timeline.ImportParams) error {
|
|
dsOpt := params.DataSourceOptions.(*Options)
|
|
|
|
// if no organizer/owner is specified, we default to the configured entity
|
|
defaultOwner := timeline.Entity{
|
|
ID: dsOpt.OwnerEntityID,
|
|
}
|
|
|
|
err := fs.WalkDir(dirEntry.FS, dirEntry.Filename, func(fpath string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if strings.HasPrefix(d.Name(), ".") {
|
|
// skip hidden files & folders
|
|
if d.IsDir() {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
return nil // traverse into subdirectories
|
|
}
|
|
|
|
// skip unsupported file types
|
|
ext := path.Ext(strings.ToLower(d.Name()))
|
|
if ext != extIcs {
|
|
return nil
|
|
}
|
|
|
|
file, err := dirEntry.FS.Open(fpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
c := gocal.NewParser(file)
|
|
|
|
// import all items (with reasonable bounds still on recurring events)
|
|
// regardless of date range (set Start/End to configure date range)
|
|
c.SkipBounds = true
|
|
|
|
if err := c.Parse(); err != nil {
|
|
// go on to the next calendar file
|
|
params.Log.Error("parsing calendar file",
|
|
zap.String("filename", fpath),
|
|
zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
for _, e := range c.Events {
|
|
var start, end time.Time
|
|
if e.Start != nil {
|
|
start = *e.Start
|
|
}
|
|
if e.End != nil {
|
|
end = *e.End
|
|
}
|
|
|
|
var loc timeline.Location
|
|
if e.Geo != nil {
|
|
loc.Latitude = &e.Geo.Lat
|
|
loc.Longitude = &e.Geo.Long
|
|
}
|
|
|
|
var owner timeline.Entity
|
|
if e.Organizer == nil {
|
|
owner = defaultOwner
|
|
} else if e.Organizer != nil && e.Organizer.Cn != "" {
|
|
owner.Name = e.Organizer.Cn
|
|
}
|
|
|
|
content := strings.TrimSpace(e.Summary)
|
|
if e.Description != "" {
|
|
if content != "" {
|
|
content += "\n"
|
|
}
|
|
content += strings.TrimSpace(e.Description)
|
|
}
|
|
|
|
params.Pipeline <- &timeline.Graph{
|
|
Item: &timeline.Item{
|
|
ID: e.Uid,
|
|
Classification: timeline.ClassEvent,
|
|
Timestamp: start,
|
|
Timespan: end,
|
|
Location: loc,
|
|
Owner: owner,
|
|
Content: timeline.ItemData{
|
|
MediaType: "text/plain",
|
|
Data: timeline.StringData(content),
|
|
},
|
|
Metadata: timeline.Metadata{
|
|
"Location": e.Location,
|
|
"Class": e.Class,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
const extIcs = ".ics"
|