github.com/wtfutil/wtf@v0.43.0/modules/gcal/client.go (about) 1 /* 2 * This butt-ugly code is direct from Google itself 3 * https://developers.google.com/calendar/quickstart/go 4 * 5 * With some changes by me to improve things a bit. 6 */ 7 8 package gcal 9 10 import ( 11 "context" 12 "encoding/json" 13 "fmt" 14 "log" 15 "net/http" 16 "os" 17 "path/filepath" 18 "sort" 19 "time" 20 21 "github.com/wtfutil/wtf/cfg" 22 "github.com/wtfutil/wtf/utils" 23 "golang.org/x/oauth2" 24 "golang.org/x/oauth2/google" 25 "google.golang.org/api/calendar/v3" 26 "google.golang.org/api/option" 27 ) 28 29 /* -------------------- Exported Functions -------------------- */ 30 31 func (widget *Widget) Fetch() ([]*CalEvent, error) { 32 ctx := context.Background() 33 34 secretPath, _ := utils.ExpandHomeDir(widget.settings.secretFile) 35 36 b, err := os.ReadFile(filepath.Clean(secretPath)) 37 if err != nil { 38 return nil, err 39 } 40 41 config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) 42 if err != nil { 43 return nil, err 44 } 45 client := getClient(ctx, config, widget.settings.email) 46 47 srv, err := calendar.NewService(context.Background(), option.WithHTTPClient(client)) 48 if err != nil { 49 return nil, err 50 } 51 52 // Get calendar events 53 var events calendar.Events 54 55 startTime := fromMidnight().Format(time.RFC3339) 56 eventLimit := int64(widget.settings.eventCount) 57 58 timezone := widget.settings.timezone 59 60 calendarIDs, err := widget.getCalendarIdList(srv) 61 for _, calendarID := range calendarIDs { 62 calendarEvents, listErr := srv.Events.List(calendarID).TimeZone(timezone).ShowDeleted(false).TimeMin(startTime).MaxResults(eventLimit).SingleEvents(true).OrderBy("startTime").Do() 63 if listErr != nil { 64 break 65 } 66 events.Items = append(events.Items, calendarEvents.Items...) 67 } 68 if err != nil { 69 return nil, err 70 } 71 72 // Sort events 73 timeDateChooser := func(event *calendar.Event) (time.Time, error) { 74 if len(event.Start.Date) > 0 { 75 return time.Parse("2006-01-02", event.Start.Date) 76 } 77 78 return time.Parse(time.RFC3339, event.Start.DateTime) 79 } 80 81 sort.Slice(events.Items, func(i, j int) bool { 82 dateA, _ := timeDateChooser(events.Items[i]) 83 dateB, _ := timeDateChooser(events.Items[j]) 84 return dateA.Before(dateB) 85 }) 86 87 // Wrap the calendar events in our custom CalEvent 88 calEvents := []*CalEvent{} 89 for _, event := range events.Items { 90 calEvents = append(calEvents, NewCalEvent(event)) 91 } 92 93 return calEvents, err 94 } 95 96 /* -------------------- Unexported Functions -------------------- */ 97 98 func fromMidnight() time.Time { 99 now := time.Now() 100 return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 101 } 102 103 // getClient uses a Context and Config to retrieve a Token 104 // then generate a Client. It returns the generated Client. 105 func getClient(ctx context.Context, config *oauth2.Config, name string) *http.Client { 106 cacheFile, err := tokenCacheFile(name) 107 if err != nil { 108 log.Fatalf("Unable to get path to cached credential file. %v", err) 109 } 110 tok, err := tokenFromFile(cacheFile) 111 if err != nil { 112 tok = getTokenFromWeb(config) 113 saveToken(cacheFile, tok) 114 } 115 return config.Client(ctx, tok) 116 } 117 118 func isAuthenticated(name string) bool { 119 cacheFile, err := tokenCacheFile(name) 120 if err != nil { 121 log.Fatalf("Unable to get path to cached credential file. %v", err) 122 } 123 _, err = tokenFromFile(cacheFile) 124 return err == nil 125 } 126 127 func (widget *Widget) authenticate() { 128 secretPath, _ := utils.ExpandHomeDir(filepath.Clean(widget.settings.secretFile)) 129 130 b, err := os.ReadFile(filepath.Clean(secretPath)) 131 if err != nil { 132 log.Fatalf("Unable to read secret file. %v", widget.settings.secretFile) 133 } 134 135 config, _ := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) 136 tok := getTokenFromWeb(config) 137 cacheFile, _ := tokenCacheFile(widget.settings.email) 138 saveToken(cacheFile, tok) 139 } 140 141 // getTokenFromWeb uses Config to request a Token. 142 // It returns the retrieved Token. 143 func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { 144 authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) 145 fmt.Printf("Go to the following link in your browser then type the "+ 146 "authorization code: \n%v (press 'return' before inserting the code)", authURL) 147 148 var code string 149 if _, err := fmt.Scan(&code); err != nil { 150 log.Fatalf("Unable to read authorization code %v", err) 151 } 152 153 tok, err := config.Exchange(context.Background(), code) 154 if err != nil { 155 log.Fatalf("Unable to retrieve token from web %v", err) 156 } 157 return tok 158 } 159 160 // tokenCacheFile generates credential file path/filename. 161 // It returns the generated credential path/filename. 162 func tokenCacheFile(name string) (string, error) { 163 configDir, err := cfg.WtfConfigDir() 164 if err != nil { 165 return "", err 166 } 167 oldFile := configDir + "/gcal-auth.json" 168 newFileName := fmt.Sprintf("%s-gcal-auth.json", name) 169 if _, err := os.Stat(oldFile); err == nil { 170 renamedFile := configDir + "/" + newFileName 171 err := os.Rename(oldFile, renamedFile) 172 if err != nil { 173 return "", err 174 } 175 return renamedFile, nil 176 } 177 return cfg.CreateFile(newFileName) 178 } 179 180 // tokenFromFile retrieves a Token from a given file path. 181 // It returns the retrieved Token and any read error encountered. 182 func tokenFromFile(file string) (*oauth2.Token, error) { 183 f, err := os.Open(filepath.Clean(file)) 184 if err != nil { 185 return nil, err 186 } 187 t := &oauth2.Token{} 188 err = json.NewDecoder(f).Decode(t) 189 defer func() { _ = f.Close() }() 190 191 return t, err 192 } 193 194 // saveToken uses a file path to create a file and store the 195 // token in it. 196 func saveToken(file string, token *oauth2.Token) { 197 fmt.Printf("Saving credential file to: %s\n", file) 198 f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 199 if err != nil { 200 log.Fatalf("unable to cache oauth token: %v", err) 201 } 202 defer func() { _ = f.Close() }() 203 204 err = json.NewEncoder(f).Encode(token) 205 if err != nil { 206 log.Fatalf("unable to encode oauth token: %v", err) 207 } 208 } 209 210 func (widget *Widget) getCalendarIdList(srv *calendar.Service) ([]string, error) { 211 // Return single calendar if settings specify we should 212 if !widget.settings.multiCalendar { 213 id, err := srv.CalendarList.Get("primary").Do() 214 if err != nil { 215 return nil, err 216 } 217 return []string{id.Id}, nil 218 } 219 220 // Get all user calendars with at the least writing access 221 var calendarIds []string 222 var pageToken string 223 for { 224 calendarList, err := srv.CalendarList.List().ShowHidden(false).MinAccessRole(widget.settings.calendarReadLevel).PageToken(pageToken).Do() 225 if err != nil { 226 return nil, err 227 } 228 for _, calendarListItem := range calendarList.Items { 229 calendarIds = append(calendarIds, calendarListItem.Id) 230 } 231 232 pageToken = calendarList.NextPageToken 233 if pageToken == "" { 234 break 235 } 236 } 237 return calendarIds, nil 238 }