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  }