Slack Timebot

stable release of Slack Timebot.

https://github.com/rickt/timebot-simple

what is Slack Timebot? at work, i very often have to know what time it is in the following regions:

  • PST
  • UTC
  • JST

so i wrote a mini go backend app that i threw into a free Google Appengine app and so now i can get the time instantly by using any of these new Slack /slash commands:

  • /time (PST)
  • /utctime (UTC)
  • /japantime (JST)

TL;DR/HOW-TO

  • you deploy a go backend app to Google Appengine that responds to
  • a couple of custom Slack /slash commands that you create in Slack

http://gist-it.appspot.com/http://github.com/rickt/timebot-simple/blob/master/timebot-simple.go

HOW-TO: use nginx maps & rewrites to redirect mobile users from product-specific pages on your desktop site to the same product-specific pages on your mobile site when desktop & mobile product ID’s & URL schemas are completely different

[i’d not seen this well-documented on the interwebs so here it is for posterity’s sakes]

problem: you have a “desktop” and a “mobile” site. they’re completely separate infra: you have two separate docroots, two separate vhosts. both sites use numeric IDs to target specific “product pages” , but because marketing departments exist and your mobile site was recently Angular-ized, your desktop and mobile sites have different URL standards and different product ID’s. to make your day even better, you’ve just been told that mobile users who hit desktop-site product URLs need to get redirected to the matching product URL on the mobile site.

the relevant URL schemas:

desktop site:

http://www.foo.com/productpages/NNNNNNN_NNN/index.html

(where N is 0-9)

mobile site:

http://m.foo.com/app/product/?p=NNNNN

(where N is 0-9)

you’re choking right now because those ID differences are just obnoxious and there’s no obvious relationship between the 6_3 digit ID’s (desktop) and 5 digit ID’s (mobile), but it’s going to be okay because your DB guys can give you a table dump with the desktop –> mobile product IDs. ok. so let’s talk specifics. your site has a product, a very thin and barely-purchased pamphlet titled “Great British Sportscars of the 1980s”. the pamphlet’s URLs:

desktop site :

/productpages/672019_029/index.html

mobile site:

/app/product/?p=40083

ok no problem right? useragent inspection in nginx is a cinch, as are nginx rewrites. you also know about nginx maps and how you very easily use them in an

"$OLDURL $NEWURL;"

way. job done, lets go home early, right?

well, all of this is true, but nginx (of course) doesn’t make it terribly obvious how you might combine useragent-based rewrites AND transforming specific ID’s from one schema another. in general, we want to rewrite that mobile user to a URL that nginx has in a map, and then that subsequent map match will send the user onto the correct mobile URL (with the correct mobile product ID) on your mobile site.

the first thing to do is to get the desktop and mobile product ID’s in a map so that nginx can do the desktop –> mobile ID transform for you. how you massage or get your data in this form is entirely up to your infrastructure and imagination, but you want to end up with a file like this:

/_mobile/productpages/672019_029/ http://m.foo.com/app/product/?p=40083;
/_mobile/productpages/562174_334/ http://m.foo.com/app/product/?p=10834;
/_mobile/productpages/455383_931/ http://m.foo.com/app/product/?p=90211;
/_mobile/productpages/369410_365/ http://m.foo.com/app/product/?p=16388;

the format (basically) is: a slightly different version of your desktop site ID’s + URI on the left, mobile site ID’s + full URL on the right. so, assuming your txt file is

/etc/nginx/vhostconf.d/www.foo.com/productids.map

, within the nginx server { } block of your www.foo.com vhost, define the map as per:

map $uri $productid_map {
    include /etc/nginx/vhostconf.d/www.foo.com/productids.map;
}

this tells nginx to setup a map, using the contents of your .map file for data. depending on the size of your map(s) you may have to increase the amount of memory (check your hash_bucket_size, map_hash_max_size, map_hash_bucket_size variable values) that nginx allocates for maps & such.

ok, so the map is setup, now we create a rewrite rule to use it. first we’ll create a variable and setup a basic mobile useragent regex match:

set $typeof_request N;
if ($http_user_agent ~ "(iPod|iPad|iPhone|BlackBerry|Android|HTC|Motorola)") {
     set $typeof_request "MOBILE";
}

if the useragent of the request matches our basic check above, the content of the $typeof_request variable is set to MOBILE. now we need to check the URI of the request to see if it matches our desktop product ID URL schema. if it does, we’ll append “_DESKREQ” to $typeof_request.

if ($request_uri ~* ^/productpages/(dddddd_ddd)/.*$) {
 set $original_productid $1;
 set $typeof_request "${typeof_request}_DESKREQ";
}

the idea here is that if we get a mobile request for a desktop URI, we want the variable $typeof_request to have the value “MOBILE_DESKREQ”. why? so we can rewrite that mobile request:

if ($typeof_request = MOBILE_DESKREQ) {
 rewrite ^ $scheme://$host/_mobile/productpages/${original_productid}/;
 break;
}

this rewrite will only occur if the request came from a mobile user and the request was for a specific syntax of URL. lets do an example. assume that someone on a mobile device requests

http://www.foo.com/productpages/672019_029/index.html

first, the $typeof_request variable would be set to MOBILE because of the $http_user_agent check matching their mobile browser useragent string. second, since the URL of the request matches our desktop product ID URL schema, _DESKREQ would be appended to $typeof_request making its value MOBILE_DESKREQ.

and so when the final $typeof_request check is done, $typeof_request is indeed set to MOBILE_DESKREQ and the URL would be rewritten to

http://www.foo.com/_mobile/productpages/672019_029/.

at this point you’re laughing because you already have an nginx map configured to look for strings like

/_mobile/productpages/672019_029/

for the express purpose of easily “mapping” them to strings like

http://m.foo.com/app/product/?p=40083.

all the pieces we need are in place, now we just ask nginx to 301 redirect to the appropriate mobile URL if the requested URI matches any of the slightly modified desktop URIs in our map:

if ($productid_map) {
 return 301 $productid_map;
}

that’s it! a quick overview of the process:

  • define a map that “maps” desktop site product ID scheme URIs (prefixed with /_mobile/) to full mobile site product ID URLs
  • check useragent for match against mobile useragents
  • check URI for match against desktop product ID URL schema
  • if both useragent and URL match, rewrite URL to “hidden” URL prefixed with /_mobile/
  • nginx map check rewrites /_mobile/ prefixed URLs to appropriate URL on mobile site
  • profit

any questions, feel free to say hi on twitter, or drop me a line.

-RMT

example Go code to pull stats from an Arma 3 server via the RCON protocol

i slapped some code together to spit out the currently-connected players on an Arma 3 server.


package main
import (
"flag"
"fmt"
steam "github.com/kidoman/go-steam"
"sort"
)
var addresses = []string{
"public.2-75thrangers.com:2303",
}
type SteamPlayers steam.PlayersInfoResponse
// implement the sort interface on a steam.PlayersInfoResponse-like type
// we can't add the sort interface to steam.PlayersInfoResponse because we're not part of the go-steam package, and
// so have to use SteamPlayers, which is a new type we created of type steam.PlayersInfoResponse for sort() purposes.
// len
func (d SteamPlayers) Len() int {
return len(d.Players)
}
// swap
func (d SteamPlayers) Swap(i, j int) {
d.Players[i], d.Players[j] = d.Players[j], d.Players[i]
}
// less
func (d SteamPlayers) Less(i, j int) bool {
return d.Players[i].Score < d.Players[j].Score
}
func main() {
// not really needed because i got lazy and put the IP in a slice as a global var <for shame>
flag.Parse()
// range through our list of IPs
for _, addr := range addresses {
// connect to the server
server, err := steam.Connect(addr)
if err != nil {
panic(err)
}
// save for later
defer server.Close()
// ping the server. if no ping, something is wrong
ping, err := server.Ping()
if err != nil {
fmt.Printf("steam: could not ping %v: %v", addr, err)
continue
}
// we're good so far. lets get the server info
info, err := server.Info()
if err != nil {
fmt.Printf("steam: could not get server info from %v: %v", addr, err)
continue
}
// still good, phew! lets get the player info
playersInfo, err := server.PlayersInfo()
if err != nil {
fmt.Printf("steam: could not get players info from %v: %v", addr, err)
continue
}
// we've got all the info we need from the server. lets print out some stuff.
fmt.Printf(" Server | %v\n", info.Name)
fmt.Printf(" Name | %s\n", info.Game)
fmt.Printf(" Ping | %s\n", ping)
fmt.Printf(" Type | %s\n", info.ServerType)
fmt.Printf("Version | %s\n", info.Version)
fmt.Printf("Players | %d/%d\n", info.Players, info.MaxPlayers)
fmt.Printf(" Map | %s\n", info.Map)
fmt.Printf(" IP | %s\n", addr)
// if there's at least 1x player, lets print out their info
if len(playersInfo.Players) > 0 {
fmt.Printf("——–|—————————————–\n")
fmt.Printf(" Score | Time On | Player Name\n")
fmt.Printf("——–|—————————————–\n")
var temp SteamPlayers
temp.Players = playersInfo.Players
sort.Sort(sort.Reverse(temp))
for _, player := range temp.Players {
fmt.Printf("%7d | %7d | %s\n", player.Score, int(player.Duration / 60), player.Name)
}
fmt.Printf("\n")
}
}
}
func must(err error) {
if err != nil {
panic(err)
}
}