Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
tildes/where/where.go
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
309 lines (253 sloc)
6.46 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"crypto/hmac" | |
"crypto/sha256" | |
"encoding/base64" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"io/ioutil" | |
"net/http" | |
"os" | |
"os/exec" | |
"regexp" | |
"strings" | |
"text/template" | |
"time" | |
"github.com/thebaer/geo" | |
"github.com/thebaer/tildes/store" | |
) | |
const ( | |
locDataJSON = "/home/bear/public_html/where.json" | |
) | |
var ( | |
hashSecret = "" | |
) | |
func main() { | |
// Get arguments | |
outFilePtr := flag.String("f", "where", "Outputted HTML filename (without .html)") | |
geocodeAPIKeyPtr := flag.String("k", "", "Google Geocoding API key") | |
hashSecretPtr := flag.String("s", "", "Secret for hashing usernames") | |
flag.Parse() | |
// Set globals | |
hashSecret = *hashSecretPtr | |
// Get online users with `who` | |
users := who() | |
// Fetch user locations based on IP address | |
for i := range users { | |
getGeo(&users[i]) | |
getFuzzyCoords(&users[i], *geocodeAPIKeyPtr) | |
} | |
// Write user coord data | |
cacheUserLocations(&users) | |
// Generate page | |
generate(users, *outFilePtr) | |
} | |
type user struct { | |
Name string `json:"name"` | |
IP string `json:"ip"` | |
Region string `json:"region"` | |
Country string `json:"country"` | |
CurrentTime string `json:"current_time"` | |
Latitude float64 `json:"lat"` | |
Longitude float64 `json:"lng"` | |
Public bool | |
Anonymous bool | |
} | |
type publicUser struct { | |
Name string `json:"name"` | |
Region string `json:"region"` | |
Country string `json:"country"` | |
Latitude float64 `json:"lat"` | |
Longitude float64 `json:"lng"` | |
} | |
var ipRegex = regexp.MustCompile("(([0-9]{1,3}[.-]){3}[0-9]{1,3})") | |
func who() []user { | |
fmt.Println("who --ips") | |
cmd := exec.Command("who", "--ips") | |
stdout, err := cmd.StdoutPipe() | |
if err != nil { | |
fmt.Println(err) | |
} | |
if err := cmd.Start(); err != nil { | |
fmt.Println(err) | |
} | |
ips := make(map[string]string) | |
r := bufio.NewReader(stdout) | |
scanner := bufio.NewScanner(r) | |
for scanner.Scan() { | |
lineParts := strings.Split(scanner.Text(), " ") | |
name := lineParts[0] | |
fmt.Println(name) | |
// Extract IP address | |
ipMatch := ipRegex.FindAllString(scanner.Text(), 1) | |
if len(ipMatch) == 0 { | |
continue | |
} | |
// Normalize any host names with dashes | |
newIp := strings.Replace(ipMatch[0], "-", ".", -1) | |
ips[newIp] = name | |
} | |
users := make([]user, len(ips)) | |
i := 0 | |
for ip, name := range ips { | |
users[i] = user{Name: name, IP: ip, Public: true, Anonymous: false} | |
// Get user permissions, marking if they're not opted-in with a | |
// `.here` file in their $HOME dir. | |
f, err := os.Stat("/home/" + name + "/.here") | |
if os.IsNotExist(err) { | |
users[i].Public = false | |
} else { | |
if f.Size() > 0 { | |
// There's something in the file! Maybe a present? | |
data, err := ioutil.ReadFile("/home/" + name + "/.here") | |
if err != nil { | |
fmt.Printf("Error reading ~/%s/.here: %v\n", name, err) | |
} else { | |
fmt.Printf("Read ~/%s/.here: %s\n", name, data) | |
// Match IP | |
ipMatch := ipRegex.FindAll(data, 1) | |
if len(ipMatch) > 0 { | |
users[i].IP = strings.TrimSpace(string(data)) | |
} | |
} | |
} | |
} | |
if _, err := os.Stat("/home/" + name + "/.somewhere"); err == nil { | |
users[i].Public = true | |
users[i].Anonymous = true | |
} | |
i++ | |
} | |
return users | |
} | |
func getTimeInZone(tz string) string { | |
cmd := exec.Command("date", "+%A %H:%M") | |
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", tz)) | |
stdout, err := cmd.StdoutPipe() | |
if err != nil { | |
fmt.Println(err) | |
} | |
if err := cmd.Start(); err != nil { | |
fmt.Println(err) | |
} | |
r := bufio.NewReader(stdout) | |
scanner := bufio.NewScanner(r) | |
for scanner.Scan() { | |
return scanner.Text() | |
} | |
return "" | |
} | |
func getGeo(u *user) { | |
fmt.Printf("Fetching %s location...\n", u.Name) | |
response, err := http.Get(fmt.Sprintf("https://freegeoip.net/json/%s", u.IP)) | |
if err != nil { | |
fmt.Printf("%s", err) | |
os.Exit(1) | |
} else { | |
defer response.Body.Close() | |
contents, err := ioutil.ReadAll(response.Body) | |
if err != nil { | |
fmt.Printf("%s", err) | |
os.Exit(1) | |
} | |
var dat map[string]interface{} | |
if err := json.Unmarshal(contents, &dat); err != nil { | |
fmt.Println(err) | |
return | |
} | |
region := dat["region_name"].(string) | |
country := dat["country_name"].(string) | |
u.CurrentTime = getTimeInZone(dat["time_zone"].(string)) | |
if u.Public { | |
u.Region = region | |
u.Country = country | |
} | |
} | |
} | |
func computeHmac256(message string) string { | |
key := []byte(hashSecret) | |
h := hmac.New(sha256.New, key) | |
h.Write([]byte(message)) | |
return base64.StdEncoding.EncodeToString(h.Sum(nil)) | |
} | |
func getFuzzyCoords(u *user, apiKey string) { | |
if !u.Public { | |
return | |
} | |
fmt.Printf("Fetching %s fuzzy coordinates...\n", u.Name) | |
loc := prettyLocation(u.Region, u.Country) | |
addr, err := geo.Geocode(loc, apiKey) | |
if err != nil { | |
fmt.Println(err) | |
return | |
} | |
u.Latitude = addr.Lat | |
u.Longitude = addr.Lng | |
} | |
func cacheUserLocations(users *[]user) { | |
// Read user data | |
res := &map[string]publicUser{} | |
if err := json.Unmarshal(store.ReadData(locDataJSON), &res); err != nil { | |
fmt.Println(err) | |
} | |
// Update user data | |
for i := range *users { | |
u := (*users)[i] | |
// Don't save users who are private | |
if !u.Public { | |
continue | |
} | |
// Hide user's name if they want to remain anonymous | |
var displayName string | |
if !u.Anonymous { | |
displayName = u.Name | |
} | |
(*res)[computeHmac256(u.Name)] = publicUser{Name: displayName, Region: u.Region, Country: u.Country, Latitude: u.Latitude, Longitude: u.Longitude} | |
// Now that we have the info we need, remove it from the page's user list | |
if u.Anonymous { | |
(*users)[i].Region = "" | |
(*users)[i].Country = "" | |
} | |
} | |
// Write user data | |
json, _ := json.Marshal(res) | |
store.WriteData(locDataJSON, json) | |
} | |
func prettyLocation(region, country string) string { | |
if region != "" { | |
return fmt.Sprintf("%s, %s", region, country) | |
} | |
return country | |
} | |
type page struct { | |
Users []user | |
Updated string | |
UpdatedForHumans string | |
} | |
func generate(users []user, outputFile string) { | |
fmt.Println("Generating page.") | |
f, err := os.Create(os.Getenv("HOME") + "/public_html/" + strings.ToLower(outputFile) + ".html") | |
if err != nil { | |
panic(err) | |
} | |
defer f.Close() | |
funcMap := template.FuncMap{ | |
"Location": prettyLocation, | |
} | |
w := bufio.NewWriter(f) | |
template, err := template.New("").Funcs(funcMap).ParseFiles("../templates/where.html") | |
if err != nil { | |
panic(err) | |
} | |
// Extra page data | |
curTime := time.Now().UTC() | |
updatedReadable := curTime.Format(time.RFC1123) | |
updated := curTime.Format(time.RFC3339) | |
// Generate the page | |
p := &page{Users: users, UpdatedForHumans: updatedReadable, Updated: updated} | |
template.ExecuteTemplate(w, "where", p) | |
w.Flush() | |
fmt.Println("DONE!") | |
} |