diff --git a/calclient.go b/account_struct.go similarity index 61% rename from calclient.go rename to account_struct.go index ce0f617..c240c64 100644 --- a/calclient.go +++ b/account_struct.go @@ -4,16 +4,62 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "log" "net/http" - calendar "google.golang.org/api/calendar/v3" - "golang.org/x/oauth2" "golang.org/x/oauth2/google" + + calendar "google.golang.org/api/calendar/v3" ) +type Account struct { + CC *CalClient + Service *calendar.Service +} + +func GetAccount(secret, token []byte) (*Account, error) { + var err error + a := new(Account) + + // Create the CalClient + a.CC = CreateClient(secret, token) + a.CC.TokenToRaw() + + // Create the Calendar Service + a.Service, err = calendar.New(a.CC.client) + if err != nil { + return nil, errors.New("Unable to retrieve calendar Client: " + err.Error()) + } + return a, nil +} + +func (a *Account) GetTodaysEvents() []Event { + var ret []Event + + return ret +} + +func (a *Account) GetDefaultCalendar() *Calendar { + c, _ := state.account.Service.CalendarList.Get("primary").Do() + return GoogleCalendarToLocal(c) +} + +func (a *Account) GetCalendarList() []Calendar { + // TODO: Check if we have the calendar list cached + var ret []Calendar + calList, err := state.account.Service.CalendarList.List().Do() + if err != nil { + return ret + } + for _, c := range calList.Items { + ret = append(ret, *GoogleCalendarToLocal(c)) + } + return ret +} + type CalClient struct { rawClientSecret []byte rawToken []byte @@ -72,7 +118,9 @@ func (c *CalClient) tokenFromWeb() error { log.Fatalf("Unable to read authorization code %v", err) } - c.tkn, err = c.cfg.Exchange(oauth2.NoContext, code) + if c.tkn, err = c.cfg.Exchange(oauth2.NoContext, code); err == nil { + c.TokenToRaw() + } return err } @@ -82,3 +130,7 @@ func (c *CalClient) TokenToRaw() { json.NewEncoder(&tmpToken).Encode(t) c.rawToken = tmpToken.Bytes() } + +func (c *CalClient) GetRawToken() []byte { + return c.rawToken +} diff --git a/calendar_struct.go b/calendar_struct.go new file mode 100644 index 0000000..bd9ddae --- /dev/null +++ b/calendar_struct.go @@ -0,0 +1,127 @@ +package main + +import ( + "time" + + calendar "google.golang.org/api/calendar/v3" +) + +type Calendar struct { + AccessRole string + BackgroundColor string + ColorId string + DefaultReminders []*calendar.EventReminder // TODO: Local Reminder struct? + Deleted bool + Description string + Etag string + ForegroundColor string + Hidden bool + Id string + Kind string + Location string + NotificationSettings *calendar.CalendarListEntryNotificationSettings // TODO: Needed? + Primary bool + Selected bool + Summary string + SummaryOverride string + TimeZone string + ForceSendFields []string // TODO: Needed? + NullFields []string //TODO: Needed? +} + +func GoogleCalendarToLocal(c *calendar.CalendarListEntry) *Calendar { + ret := Calendar{ + AccessRole: c.AccessRole, + BackgroundColor: c.BackgroundColor, + ColorId: c.ColorId, + DefaultReminders: c.DefaultReminders, + Deleted: c.Deleted, + Description: c.Description, + Etag: c.Etag, + ForegroundColor: c.ForegroundColor, + Hidden: c.Hidden, + Id: c.Id, + Kind: c.Kind, + Location: c.Location, + Primary: c.Primary, + Selected: c.Selected, + Summary: c.Summary, + SummaryOverride: c.SummaryOverride, + TimeZone: c.TimeZone, + } + return &ret +} + +func (c *Calendar) GetTodaysEvents() []Event { + var ret []Event + + minTime := time.Date( + time.Now().Year(), + time.Now().Month(), + time.Now().Day(), + 0, 0, 0, 0, time.Local, + ) + maxTime := time.Date( + time.Now().Year(), + time.Now().Month(), + time.Now().Day(), + 23, 59, 59, 999999999, time.Local, + ) + events, err := state.account.Service.Events.List(c.Id). + ShowDeleted(false). + SingleEvents(true). + TimeMin(minTime.Format(time.RFC3339)). + TimeMax(maxTime.Format(time.RFC3339)). + OrderBy("startTime"). + Do() + if err != nil { + return ret + } + for _, e := range events.Items { + ret = append(ret, *GoogleEventToLocal(e)) + } + return ret +} + +func (c *Calendar) GetCalendarEvents() []Event { + var ret []Event + events, err := state.account.Service.Events.List(c.Id).ShowDeleted(false). + SingleEvents(true).OrderBy("startTime").Do() + if err != nil { + return ret + } + for _, e := range events.Items { + ret = append(ret, *GoogleEventToLocal(e)) + } + return ret +} + +/* For Adding a private copy of an existing event +func (c *Calendar) ImportEvent(event *Event) error { + _, err := state.account.Service.Events.Import(c.Id, event).Do() + return err +} +*/ +func (c *Calendar) InsertEvent(event *Event) (*Event, error) { + e, err := state.account.Service.Events.Insert(c.Id, LocalEventToGoogle(event)).Do() + return GoogleEventToLocal(e), err +} + +func (c *Calendar) DeleteEvent(eId string) error { + return state.account.Service.Events.Delete(c.Id, eId).Do() +} + +func (c *Calendar) GetEvent(eId string) (*Event, error) { + e, err := state.account.Service.Events.Get(c.Id, eId).Do() + return GoogleEventToLocal(e), err +} + +func (c *Calendar) QuickAdd(text string) (*Event, error) { + e, err := state.account.Service.Events.QuickAdd(c.Id, text).Do() + return GoogleEventToLocal(e), err +} + +func (c *Calendar) Update(eId string, event *Event) (*Event, error) { + e, err := state.account.Service.Events.Update(c.Id, eId, LocalEventToGoogle(event)).Do() + return GoogleEventToLocal(e), err +} diff --git a/event_struct.go b/event_struct.go new file mode 100644 index 0000000..47a66e2 --- /dev/null +++ b/event_struct.go @@ -0,0 +1,240 @@ +package main + +import ( + "fmt" + "time" + + calendar "google.golang.org/api/calendar/v3" +) + +type Event struct { + //Attachments []EventAttachment // Google Drive files + Attendees []EventAttendee + ColorId string + Created string + Creator *EventAttendee + Description string + End *calendar.EventDateTime + EndTimeUnspecified bool + Etag string + //ExtendedProperties []EventExtendedProperties + GuestsCanInviteOthers bool + GuestsCanModify bool + GuestsCanSeeOtherGuests bool + HangoutLink string + HtmlLink string + ICalUID string + Id string + Kind string + Location string + Locked bool + Organizer *EventAttendee + OriginalStartTime *calendar.EventDateTime + PrivateCopy bool + Recurrence []string + RecurringEventId string + //Reminders *EventReminders + Sequence int64 + //Source *EventSource + Start *calendar.EventDateTime + Status string + Summary string + Transparency string + Updated string + Visibility string +} + +func (e *Event) ToCLIString() string { + return fmt.Sprintf("%s\n%s\n", e.Summary, e.GetStartTime()) +} + +func (e *Event) GetStartTime() string { + tm, err := time.Parse(time.RFC3339, e.Start.DateTime) + if err != nil { + return "00:00:00" + } + return tm.Local().Format("15:04:05") +} + +func GoogleEventToLocal(e *calendar.Event) *Event { + return &Event{ + //Attachments []EventAttachment // Google Drive files + Attendees: GoogleAttendeeSliceToLocal(e.Attendees), + ColorId: e.ColorId, + Created: e.Created, + Creator: GoogleCreatorToLocal(e.Creator), + Description: e.Description, + End: e.End, + EndTimeUnspecified: e.EndTimeUnspecified, + Etag: e.Etag, + //ExtendedProperties []EventExtendedProperties + //GuestsCanInviteOthers: *e.GuestsCanInviteOthers, + GuestsCanModify: e.GuestsCanModify, + //GuestsCanSeeOtherGuests: *e.GuestsCanSeeOtherGuests, + HangoutLink: e.HangoutLink, + HtmlLink: e.HtmlLink, + ICalUID: e.ICalUID, + Id: e.Id, + Kind: e.Kind, + Location: e.Location, + Locked: e.Locked, + Organizer: GoogleOrganizerToLocal(e.Organizer), + OriginalStartTime: e.OriginalStartTime, + PrivateCopy: e.PrivateCopy, + Recurrence: e.Recurrence, + RecurringEventId: e.RecurringEventId, + //Reminders *EventReminders + Sequence: e.Sequence, + //Source *EventSource + Start: e.Start, + Status: e.Status, + Summary: e.Summary, + Transparency: e.Transparency, + Updated: e.Updated, + Visibility: e.Visibility, + } +} + +func LocalEventToGoogle(e *Event) *calendar.Event { + return &calendar.Event{ + //Attachments []EventAttachment // Google Drive files + //Attendees: GoogleAttendeeSliceToLocal(e.Attendees), + ColorId: e.ColorId, + Created: e.Created, + Creator: LocalCreatorToGoogle(e.Creator), + Description: e.Description, + End: e.End, + EndTimeUnspecified: e.EndTimeUnspecified, + Etag: e.Etag, + //ExtendedProperties []EventExtendedProperties + //GuestsCanInviteOthers: *e.GuestsCanInviteOthers, + GuestsCanModify: e.GuestsCanModify, + //GuestsCanSeeOtherGuests: *e.GuestsCanSeeOtherGuests, + HangoutLink: e.HangoutLink, + HtmlLink: e.HtmlLink, + ICalUID: e.ICalUID, + Id: e.Id, + Kind: e.Kind, + Location: e.Location, + Locked: e.Locked, + Organizer: LocalOrganizerToGoogle(e.Organizer), + OriginalStartTime: e.OriginalStartTime, + PrivateCopy: e.PrivateCopy, + Recurrence: e.Recurrence, + RecurringEventId: e.RecurringEventId, + //Reminders *EventReminders + Sequence: e.Sequence, + //Source *EventSource + Start: e.Start, + Status: e.Status, + Summary: e.Summary, + Transparency: e.Transparency, + Updated: e.Updated, + Visibility: e.Visibility, + } +} + +type EventAttendee struct { + AdditionalGuests int64 + Comment string + DisplayName string + Email string + Id string + Optional bool + Organizer bool + Resource bool + ResponseStatus string + Self bool + EventCreator bool + EventOrganizer bool +} + +func GoogleAttendeeSliceToLocal(a []*calendar.EventAttendee) []EventAttendee { + var ret []EventAttendee + for _, e := range a { + ret = append(ret, *GoogleAttendeeToLocal(e)) + } + return ret +} + +func LocalAttendeeSliceToGoogle(a []EventAttendee) []*calendar.EventAttendee { + var ret []*calendar.EventAttendee + for _, e := range a { + ret = append(ret, LocalAttendeeToGoogle(&e)) + } + return ret +} + +func GoogleAttendeeToLocal(a *calendar.EventAttendee) *EventAttendee { + return &EventAttendee{ + AdditionalGuests: a.AdditionalGuests, + Comment: a.Comment, + DisplayName: a.DisplayName, + Email: a.Email, + Id: a.Id, + Optional: a.Optional, + Organizer: a.Organizer, + Resource: a.Resource, + ResponseStatus: a.ResponseStatus, + Self: a.Self, + } +} + +func LocalAttendeeToGoogle(a *EventAttendee) *calendar.EventAttendee { + return &calendar.EventAttendee{ + AdditionalGuests: a.AdditionalGuests, + Comment: a.Comment, + DisplayName: a.DisplayName, + Email: a.Email, + Id: a.Id, + Optional: a.Optional, + Organizer: a.Organizer, + Resource: a.Resource, + ResponseStatus: a.ResponseStatus, + Self: a.Self, + } +} + +func GoogleOrganizerToLocal(a *calendar.EventOrganizer) *EventAttendee { + return &EventAttendee{ + DisplayName: a.DisplayName, + Email: a.Email, + Id: a.Id, + Self: a.Self, + EventOrganizer: true, + } +} + +func LocalOrganizerToGoogle(a *EventAttendee) *calendar.EventOrganizer { + return &calendar.EventOrganizer{ + DisplayName: a.DisplayName, + Email: a.Email, + Id: a.Id, + Self: a.Self, + } +} + +func GoogleCreatorToLocal(a *calendar.EventCreator) *EventAttendee { + return &EventAttendee{ + DisplayName: a.DisplayName, + Email: a.Email, + Id: a.Id, + Self: a.Self, + EventCreator: true, + } +} + +func LocalCreatorToGoogle(a *EventAttendee) *calendar.EventCreator { + return &calendar.EventCreator{ + DisplayName: a.DisplayName, + Email: a.Email, + Id: a.Id, + Self: a.Self, + } +} + +type ByStartTime []Event + +func (a ByStartTime) Len() int { return len(a) } +func (a ByStartTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByStartTime) Less(i, j int) bool { return a[i].Start.DateTime < a[j].Start.DateTime } diff --git a/main.go b/main.go index 366b043..5376a49 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,8 @@ import ( "io/ioutil" "log" "os" - "time" - - calendar "google.golang.org/api/calendar/v3" + "sort" + "strings" "github.com/br0xen/user-config" ) @@ -22,12 +21,14 @@ type AppState struct { Version int ClientSecret []byte cfg *userConfig.Config + account *Account } var state *AppState func main() { var err error + var op string state = &AppState{Name: AppName, Version: AppVersion} state.cfg, err = userConfig.NewConfig(state.Name) @@ -35,75 +36,118 @@ func main() { panic(err) } - for i := range os.Args { - switch { - case os.Args[i] == "--reinit": - // Reset all config - for _, v := range state.cfg.GetKeyList() { - state.cfg.DeleteKey(v) - } + if len(os.Args) > 1 { + op = os.Args[1] + switch os.Args[1] { + case "--reinit": } + } else { + op = "today" } DoVersionCheck() + switch op { + case "--reinit": + // Reset all config + for _, v := range state.cfg.GetKeyList() { + fmt.Println("Deleting Key: " + v) + state.cfg.DeleteKey(v) + } + case "today": + // Show everything on the calendar for today + InitComm() + // TODO: Get calendars flagged as default + list := state.account.GetCalendarList() + var todayEvents []Event + if len(list) > 0 { + for i := range list { + if list[i].Deleted { + // Deleted calendar, next please + continue + } + todayEvents = append(todayEvents, list[i].GetTodaysEvents()...) + } + sort.Sort(ByStartTime(todayEvents)) + for _, e := range todayEvents { + if e.GetStartTime() == "00:00:00" { + fmt.Println("[All Day ] " + e.Summary) + } else { + fmt.Println("[" + e.GetStartTime() + "] " + e.Summary) + } + } + } else { + fmt.Printf("No calendars found.\n") + } + + case "defaults": + // Show Defaults + InitComm() + + case "add": + // Quick event add to primary calendar + quickAddText := strings.Join(os.Args[2:], " ") + e, err := state.account.GetDefaultCalendar().QuickAdd(quickAddText) + if err != nil { + fmt.Println(err.Error()) + } else { + fmt.Println(e.ToCLIString()) + } + case "bail": + // Just initialize communications and bail + InitComm() + } + + //fmt.Println("\n-====-\n") + + /* + t := time.Now().Format(time.RFC3339) + events, err := srv.Events.List("primary").ShowDeleted(false). + SingleEvents(true).TimeMin(t).MaxResults(10).OrderBy("startTime").Do() + if err != nil { + log.Fatalf("Unable to retrieve next ten of the user's events. %v", err) + } + + fmt.Println("Upcoming events:") + if len(events.Items) > 0 { + for _, i := range events.Items { + var when string + // If the DateTime is an empty string the Event is an all-day Event. + // So only Date is available. + if i.Start.DateTime != "" { + when = i.Start.DateTime + } else { + when = i.Start.Date + } + fmt.Printf("%s (%s)\n", i.Summary, when) + } + } else { + fmt.Printf("No upcoming events found.\n") + } + */ +} + +func InitComm() { + var err error sec := state.cfg.GetBytes("ClientSecret") tkn := state.cfg.GetBytes("Token") - c := CreateClient(sec, tkn) - - c.TokenToRaw() - state.cfg.SetBytes("Token", c.rawToken) - - srv, err := calendar.New(c.client) - + state.account, err = GetAccount(sec, tkn) if err != nil { - log.Fatalf("unable to retrieve calendar Client %v", err) + log.Fatalf("Unable to get Account: %v", err) } - listSvc := srv.CalendarList.List() - list, err := listSvc.Do() - if err != nil { - log.Fatalf("unable to retrieve calendar list %v", err) - } - if len(list.Items) > 0 { - for i := range list.Items { - if list.Items[i].Deleted { - // Deleted calendar, next please - continue - } - fmt.Print("(" + list.Items[i].Id + ") ") - fmt.Print(list.Items[i].Summary + "; ") - fmt.Println(list.Items[i].Description) + // Save the Raw Token + state.cfg.SetBytes("Token", state.account.CC.GetRawToken()) + + // If the 'defaultCalendars' cfg is set to 'primary', we need to actually but the primary cal id in there + var defCal []string + defCal, err = state.cfg.GetArray("defaultCalendars") + if len(defCal) == 0 || (len(defCal) == 1 && defCal[0] == "primary") { + gCal, err := state.account.Service.CalendarList.Get("primary").Do() + if err == nil { + state.cfg.SetArray("defaultCalendars", []string{gCal.Id}) } - } else { - fmt.Printf("No calendars found.\n") - } - - fmt.Println("\n-====-\n") - - t := time.Now().Format(time.RFC3339) - events, err := srv.Events.List("primary").ShowDeleted(false). - SingleEvents(true).TimeMin(t).MaxResults(10).OrderBy("startTime").Do() - if err != nil { - log.Fatalf("Unable to retrieve next ten of the user's events. %v", err) - } - - fmt.Println("Upcoming events:") - if len(events.Items) > 0 { - for _, i := range events.Items { - var when string - // If the DateTime is an empty string the Event is an all-day Event. - // So only Date is available. - if i.Start.DateTime != "" { - when = i.Start.DateTime - } else { - when = i.Start.Date - } - fmt.Printf("%s (%s)\n", i.Summary, when) - } - } else { - fmt.Printf("No upcoming events found.\n") } } @@ -127,6 +171,7 @@ func DoVersionCheck() { } } state.cfg.SetInt("version", 1) + state.cfg.SetArray("defaultCalendars", []string{"primary"}) } state.ClientSecret = state.cfg.GetBytes("ClientSecret") // Refetch the version from the config