blob: dde634163731993f8e75e3acda6f16d082ae3a86 [file] [log] [blame]
Jess Frazelle1f12d9f2018-04-16 16:31:21 -04001package main
2
3import (
Jess Frazellee74be1b2018-04-16 17:49:15 -04004 "context"
Jess Frazelle1f12d9f2018-04-16 16:31:21 -04005 "flag"
6 "fmt"
Jess Frazellee74be1b2018-04-16 17:49:15 -04007 "io/ioutil"
Jess Frazelle1f12d9f2018-04-16 16:31:21 -04008 "os"
9 "os/signal"
Jess Frazellee74be1b2018-04-16 17:49:15 -040010 "os/user"
11 "path/filepath"
Jess Frazelle3bc2ca32018-04-16 19:23:06 -040012 "strconv"
Jess Frazelle36c490c2018-04-16 18:22:40 -040013 "strings"
Jess Frazelle1f12d9f2018-04-16 16:31:21 -040014 "syscall"
15 "time"
16
17 "github.com/jessfraz/tripitcalb0t/tripit"
18 "github.com/jessfraz/tripitcalb0t/version"
Jess Frazelle7f0176f2018-04-16 20:55:42 -040019 "github.com/mmcloughlin/openflights"
Jess Frazelle1f12d9f2018-04-16 16:31:21 -040020 "github.com/sirupsen/logrus"
Jess Frazellee74be1b2018-04-16 17:49:15 -040021 "golang.org/x/oauth2/google"
22 calendar "google.golang.org/api/calendar/v3"
Jess Frazelle1f12d9f2018-04-16 16:31:21 -040023)
24
25const (
26 // BANNER is what is printed for help/info output.
Jess Frazelle42bb5172018-04-16 16:41:17 -040027 BANNER = ` _ _ _ _ _ _ ___ _
28| |_ _ __(_)_ __ (_) |_ ___ __ _| | |__ / _ \| |_
29| __| '__| | '_ \| | __/ __/ _` + "`" + ` | | '_ \| | | | __|
30| |_| | | | |_) | | || (_| (_| | | |_) | |_| | |_
31 \__|_| |_| .__/|_|\__\___\__,_|_|_.__/ \___/ \__|
32 |_|
Jess Frazelle1f12d9f2018-04-16 16:31:21 -040033
34 Bot to automatically create Google Calendar events from TripIt flight data.
35 Version: %s
36 Build: %s
37
38`
39)
40
41var (
Jess Frazellee74be1b2018-04-16 17:49:15 -040042 googleCalendarKeyfile string
43 calendarName string
44 credsDir string
45
Jess Frazelle1f12d9f2018-04-16 16:31:21 -040046 tripitUsername string
47 tripitToken string
48
49 interval string
50 once bool
51
52 debug bool
53 vrsn bool
54)
55
56func init() {
Jess Frazellee74be1b2018-04-16 17:49:15 -040057 // Get home directory.
58 home, err := getHome()
59 if err != nil {
60 logrus.Fatal(err)
61 }
62 credsDir = filepath.Join(home, ".tripitcalb0t")
63
Jess Frazelle1f12d9f2018-04-16 16:31:21 -040064 // parse flags
Jess Frazellee74be1b2018-04-16 17:49:15 -040065 flag.StringVar(&googleCalendarKeyfile, "google-keyfile", filepath.Join(credsDir, "google.json"), "Path to Google Calendar keyfile")
66 flag.StringVar(&calendarName, "calendar", os.Getenv("GOOGLE_CALENDAR_ID"), "Calendar name to add events to (or env var GOOGLE_CALENDAR_ID)")
67
Jess Frazelle1f12d9f2018-04-16 16:31:21 -040068 flag.StringVar(&tripitUsername, "tripit-username", os.Getenv("TRIPIT_USERNAME"), "TripIt Username for authentication (or env var TRIPIT_USERNAME)")
69 flag.StringVar(&tripitToken, "tripit-token", os.Getenv("TRIPIT_TOKEN"), "TripIt Token for authentication (or env var TRIPIT_TOKEN)")
70
71 flag.StringVar(&interval, "interval", "1m", "update interval (ex. 5ms, 10s, 1m, 3h)")
72 flag.BoolVar(&once, "once", false, "run once and exit, do not run as a daemon")
73
74 flag.BoolVar(&vrsn, "version", false, "print version and exit")
75 flag.BoolVar(&vrsn, "v", false, "print version and exit (shorthand)")
76 flag.BoolVar(&debug, "d", false, "run in debug mode")
77
78 flag.Usage = func() {
79 fmt.Fprint(os.Stderr, fmt.Sprintf(BANNER, version.VERSION, version.GITCOMMIT))
80 flag.PrintDefaults()
81 }
82
83 flag.Parse()
84
85 if vrsn {
86 fmt.Printf("tripitcalb0t version %s, build %s", version.VERSION, version.GITCOMMIT)
87 os.Exit(0)
88 }
89
90 // set log level
91 if debug {
92 logrus.SetLevel(logrus.DebugLevel)
93 }
94
95 if tripitUsername == "" {
96 usageAndExit("tripit username cannot be empty", 1)
97 }
98
99 if tripitToken == "" {
100 usageAndExit("tripit token cannot be empty", 1)
101 }
102
Jess Frazellee74be1b2018-04-16 17:49:15 -0400103 if _, err := os.Stat(googleCalendarKeyfile); os.IsNotExist(err) {
104 usageAndExit(fmt.Sprintf("Google Calendar keyfile %q does not exist", googleCalendarKeyfile), 1)
105 }
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400106}
107
108func main() {
109 var ticker *time.Ticker
110
111 // On ^C, or SIGTERM handle exit.
112 c := make(chan os.Signal, 1)
113 signal.Notify(c, os.Interrupt)
114 signal.Notify(c, syscall.SIGTERM)
115 go func() {
116 for sig := range c {
117 ticker.Stop()
118 logrus.Infof("Received %s, exiting.", sig.String())
119 os.Exit(0)
120 }
121 }()
122
123 // Parse the duration.
124 dur, err := time.ParseDuration(interval)
125 if err != nil {
126 logrus.Fatalf("parsing %s as duration failed: %v", interval, err)
127 }
128 ticker = time.NewTicker(dur)
129
130 // Create the TripIt API client.
Jess Frazellee74be1b2018-04-16 17:49:15 -0400131 tripitClient := tripit.New(tripitUsername, tripitToken)
132
133 // Create the Google calendar API client.
134 gcalData, err := ioutil.ReadFile(googleCalendarKeyfile)
135 if err != nil {
136 logrus.Fatalf("reading file %s failed: %v", googleCalendarKeyfile, err)
137 }
Jess Frazelle12b27922018-04-16 19:55:27 -0400138 gcalTokenSource, err := google.JWTConfigFromJSON(gcalData, calendar.CalendarScope)
Jess Frazellee74be1b2018-04-16 17:49:15 -0400139 if err != nil {
140 logrus.Fatalf("creating google calendar token source from file %s failed: %v", googleCalendarKeyfile, err)
141 }
142
143 // Create our context.
144 ctx := context.Background()
145
146 // Create the Google calendar client.
147 gcalClient, err := calendar.New(gcalTokenSource.Client(ctx))
148 if err != nil {
149 logrus.Fatalf("creating google calendar client failed: %v", err)
150 }
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400151
152 // If the user passed the once flag, just do the run once and exit.
153 if once {
Jess Frazelle36c490c2018-04-16 18:22:40 -0400154 run(tripitClient, gcalClient, calendarName)
Jess Frazelle12b27922018-04-16 19:55:27 -0400155 logrus.Infof("Updated TripIt calendar entries in Google calendar %s", calendarName)
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400156 os.Exit(0)
157 }
158
Jess Frazelle36c490c2018-04-16 18:22:40 -0400159 logrus.Infof("Starting bot to update TripIt calendar entries in Google calendar %s every %s", calendarName, interval)
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400160 for range ticker.C {
Jess Frazelle36c490c2018-04-16 18:22:40 -0400161 run(tripitClient, gcalClient, calendarName)
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400162 }
163}
164
Jess Frazelle36c490c2018-04-16 18:22:40 -0400165func run(tripitClient *tripit.Client, gcalClient *calendar.Service, calendarName string) {
166 // Get a list of events from Google calendar.
Jess Frazelledc432aa2018-04-16 21:03:15 -0400167 t := time.Now().AddDate(-4, 0, 0).Format(time.RFC3339)
168 events, err := gcalClient.Events.List(calendarName).ShowDeleted(false).SingleEvents(true).TimeMin(t).OrderBy("startTime").Q("Flight").MaxResults(2500).Do()
Jess Frazellee74be1b2018-04-16 17:49:15 -0400169 if err != nil {
170 logrus.Fatalf("getting events from google calendar %s failed: %v", calendarName, err)
171 }
Jess Frazellee74be1b2018-04-16 17:49:15 -0400172
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400173 trips, err := getTripItEvents(tripitClient, 1, "true")
174 if err != nil {
175 logrus.Fatalf("getting tripit events failed: %v", err)
176 }
177
Jess Frazelle12b27922018-04-16 19:55:27 -0400178 // Iterate over the trip and see if we already have a matching calendar event.
179 // If not make one and/or update the old one.
180 for _, trip := range trips {
Jess Frazelle7f0176f2018-04-16 20:55:42 -0400181 if trip.ConfirmationNumber == "" {
182 logrus.Warnf("skipping trip that has no confirmation number: %#v", trip)
183 continue
184 }
185
Jess Frazelle12b27922018-04-16 19:55:27 -0400186 var matchingEvent *calendar.Event
187 for _, e := range events.Items {
188 // We only care about TripIt events that match our tripID or segmentID.
189 if (strings.Contains(strings.ToLower(e.Description), "tripit") ||
190 strings.Contains(strings.ToLower(e.Summary), "flight")) &&
Jess Frazelle75ad88c2018-04-16 20:59:43 -0400191 strings.Contains(e.Description, trip.SegmentID) {
Jess Frazelle12b27922018-04-16 19:55:27 -0400192 matchingEvent = e
193 break
194 }
195 }
196
Jess Frazelle7f0176f2018-04-16 20:55:42 -0400197 // Get airport information.
198 airport := getAirportName(trip.AirportCode)
199 if airport == "" {
200 logrus.Errorf("getting airport information from iata database for %s returned no match", trip.AirportCode)
201 continue
202 }
203
Jess Frazelle12b27922018-04-16 19:55:27 -0400204 if matchingEvent == nil {
Jess Frazelle7f0176f2018-04-16 20:55:42 -0400205 // No event was found for this trip, let's create one.
206 matchingEvent = &calendar.Event{
207 Summary: trip.Title,
208 Description: trip.Description,
209 Start: &trip.Start,
210 End: &trip.End,
211 Location: airport,
212 }
213
214 // Insert the event.
215 _, err = gcalClient.Events.Insert(calendarName, matchingEvent).Do()
216 if err != nil {
217 logrus.Errorf("inserting google calendar event failed: %v", err)
218 }
Jess Frazelle12b27922018-04-16 19:55:27 -0400219 continue
220 }
221
222 // Update our matching event.
223 matchingEvent.Summary = trip.Title
224 matchingEvent.Description = trip.Description
225 matchingEvent.Start = &trip.Start
226 matchingEvent.End = &trip.End
Jess Frazelle7f0176f2018-04-16 20:55:42 -0400227 matchingEvent.Location = airport
228
229 // Update the event.
230 _, err = gcalClient.Events.Update(calendarName, matchingEvent.Id, matchingEvent).Do()
Jess Frazelle12b27922018-04-16 19:55:27 -0400231 if err != nil {
232 logrus.Errorf("updating google calendar event %s failed: %v", matchingEvent.Id, err)
233 }
234 }
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400235}
236
237func getTripItEvents(tripitClient *tripit.Client, page int, pastFilter string) ([]tripit.Event, error) {
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400238 // Get a list of trips.
Jess Frazellee74be1b2018-04-16 17:49:15 -0400239 resp, err := tripitClient.ListTrips(
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400240 tripit.Filter{
241 Type: tripit.FilterPast,
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400242 Value: pastFilter,
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400243 },
244 tripit.Filter{
245 Type: tripit.FilterIncludeObjects,
246 Value: "true",
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400247 },
248 tripit.Filter{
249 Type: tripit.FilterPageNum,
250 Value: fmt.Sprintf("%d", page),
251 },
252 tripit.Filter{
253 Type: tripit.FilterPageSize,
254 Value: "25",
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400255 })
256 if err != nil {
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400257 return nil, fmt.Errorf("listing trips from TripIt failed: %v", err)
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400258 }
259
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400260 var events []tripit.Event
261
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400262 // Iterate over our flights and create/update calendar entries in Google calendar.
263 for _, flight := range resp.Flights {
264 // Create the events for the flight.
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400265 evs, err := flight.GetFlightSegmentsAsEvents()
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400266 if err != nil {
267 // Warn on error and continue iterating through the flights.
268 logrus.Warn(err)
269 continue
270 }
271
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400272 // Add to our events array.
273 events = append(events, evs...)
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400274 }
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400275
276 // Paginate.
277 pageNum, err := strconv.Atoi(resp.PageNum)
278 if err != nil {
279 return nil, err
280 }
281 maxPage, err := strconv.Atoi(resp.MaxPage)
282 if err != nil {
283 return nil, err
284 }
285
286 if pageNum < maxPage {
Jess Frazelle3bc2ca32018-04-16 19:23:06 -0400287 pageNum++
288
289 evs, err := getTripItEvents(tripitClient, pageNum, pastFilter)
290 if err != nil {
291 return nil, err
292 }
293
294 return append(events, evs...), nil
295 }
296
297 if pastFilter == "true" {
298 // Get future events as well.
299 evs, err := getTripItEvents(tripitClient, 1, "false")
300 if err != nil {
301 return nil, err
302 }
303
304 return append(events, evs...), nil
305 }
306
307 return events, nil
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400308}
309
Jess Frazelle7f0176f2018-04-16 20:55:42 -0400310func getAirportName(code string) string {
311 for _, airport := range openflights.Airports {
312 if airport.IATA == code {
313 return airport.Name
314 }
315 }
316
317 return ""
318}
319
Jess Frazelle1f12d9f2018-04-16 16:31:21 -0400320func usageAndExit(message string, exitCode int) {
321 if message != "" {
322 fmt.Fprintf(os.Stderr, message)
323 fmt.Fprintf(os.Stderr, "\n\n")
324 }
325 flag.Usage()
326 fmt.Fprintf(os.Stderr, "\n")
327 os.Exit(exitCode)
328}
Jess Frazellee74be1b2018-04-16 17:49:15 -0400329
330func getHome() (string, error) {
331 home := os.Getenv(homeKey)
332 if home != "" {
333 return home, nil
334 }
335
336 u, err := user.Current()
337 if err != nil {
338 return "", err
339 }
340 return u.HomeDir, nil
341}