trains

Notifications for the new MBTA trains. Probably no longer functional.

git clone https://code.pdelong.com/trains.git

  1package main
  2
  3import (
  4	"database/sql"
  5	"flag"
  6	"fmt"
  7	"log"
  8	"net/http"
  9	"os"
 10	"strings"
 11	"time"
 12
 13	_ "github.com/lib/pq"
 14)
 15
 16func UpdateTrainsInDatabase(db *sql.DB, time time.Time, trains []Train) {
 17	for _, train := range trains {
 18		for _, car := range train.Cars {
 19			if err := InsertOrUpdateCarInDatabase(db, car, time); err != nil {
 20				log.Println(err)
 21			}
 22		}
 23	}
 24}
 25
 26func InsertOrUpdateCarInDatabase(db *sql.DB, c int, t time.Time) error {
 27	iou := `
 28	INSERT INTO cars (car, last_seen)
 29	VALUES ($1, $2)
 30	ON CONFLICT (car) DO UPDATE
 31		SET last_seen = $2
 32	`
 33
 34	_, err := db.Exec(iou, c, t)
 35	if err != nil {
 36		return err
 37	}
 38
 39	return nil
 40}
 41
 42func ReadCarsFromDatabase(db *sql.DB) (map[int]time.Time, error) {
 43	query := `
 44	SELECT car, last_seen
 45	FROM cars
 46	`
 47
 48	rows, err := db.Query(query)
 49	if err != nil {
 50		return nil, err
 51	}
 52
 53	cars := make(map[int]time.Time)
 54
 55	for rows.Next() {
 56		var car int
 57		var lastSeen time.Time
 58		if err := rows.Scan(&car, &lastSeen); err != nil {
 59			panic(err)
 60		}
 61		cars[car] = lastSeen
 62	}
 63
 64	if err = rows.Close(); err != nil {
 65		return nil, err
 66	}
 67
 68	return cars, nil
 69}
 70
 71func ConnectToDatabase(url string) *sql.DB {
 72	db, err := sql.Open("postgres", url)
 73	if err != nil {
 74		panic(err)
 75	}
 76
 77	if err = db.Ping(); err != nil {
 78		panic(err)
 79	}
 80
 81	return db
 82}
 83
 84func SendMessages(url string, trains []Train, cars map[int]time.Time, time time.Time) {
 85	log.Println("Found new trains: ")
 86	for _, train := range trains {
 87		log.Println(train)
 88
 89		found := false
 90		for _, car := range train.Cars {
 91			// If the we haven't seen the car in a few minutes, it's probably running a new trip.
 92			delta := time.Sub(cars[car])
 93			if delta.Seconds() <= 60*5 {
 94				log.Printf(
 95					"Saw car too recently (%v seconds ago) to send a notification for it",
 96					delta.Seconds())
 97				continue
 98			}
 99			found = true
100		}
101
102		// Alert for the entire train if we found a single car in it that's notifiable
103		if !found {
104			continue
105		}
106		resp, err := http.Post(
107			url,
108			"application/json",
109			strings.NewReader(
110				fmt.Sprintf("{\"value1\": %q}", train)))
111		if err != nil {
112			log.Printf("Unable to send IFTTT message: %v", err)
113		}
114		resp.Body.Close()
115		log.Println("Successfully sent message!")
116	}
117}
118
119func main() {
120	var (
121		IFTTTURL    = flag.String("ifttt_url", "", "The IFTTT URL to send the notification to.")
122		DatabaseURL = flag.String("database_url", "", "The database connection string.")
123	)
124
125	flag.Parse()
126
127	if *IFTTTURL == "" {
128		fmt.Println("ifttt_url must be set")
129		os.Exit(1)
130	}
131
132	if *DatabaseURL == "" {
133		fmt.Println("database_url must be set")
134		os.Exit(1)
135	}
136
137	log.Println("Connecting to database")
138	db := ConnectToDatabase(*DatabaseURL)
139	defer db.Close()
140	log.Println("Successfully connected to database")
141
142	trains, err := GetTrains()
143	if err != nil {
144		panic(fmt.Sprintf("Unable to get trains: %v", err))
145	}
146
147	log.Println("Found trains: ")
148	for _, train := range trains {
149		log.Println(train)
150	}
151
152	newTrains := FilterNewTrains(trains)
153	if len(newTrains) == 0 {
154		log.Println("Didn't find any trains with new cars :(")
155		return
156	}
157
158	// Read before we update the database with the new trips.
159	cars, err := ReadCarsFromDatabase(db)
160	if err != nil {
161		panic(err)
162	}
163
164	now := time.Now().UTC()
165	SendMessages(*IFTTTURL, newTrains, cars, now)
166
167	// We want to update the database last so we don't miss any notifications.
168	UpdateTrainsInDatabase(db, now, trains)
169}