The purpose of the Yik Yak status API is to allow a third-party application to set the status of a Yik Yak user programmatically. A secondary purpose is to allow a third-party application to set the location of the user (latitude, longitude), even though this is less likely to be less useful to most applications.
Let's assume you are an application developer, Joe. Your app, class2day allows college students to go to your website (perhaps, http://joesapps.com/class2day or similar) and enter their class schedule for the current term. They can also add outside activities, like "flag football practice" or "spanish club lunch". Once entered, class2day will constantly run and monitor the time and when the student should (hah!) be in class or at an activity automatically set the student's Yik Yak status to reflect that: "#organicChem322" or "flagFootballPractice".
Outside of building class2day itself, two preconditions have to be met for Joe's app to work correctly with YikYak. First, Joe must become a registered developer with Yik Yak. Once registered and approved by Yik Yak, Joe will be issued a developer id and a (secret) developer key. Because only Joe and Yik Yak know the developer key, Joe can use it to send messages to Yik Yak servers that can be checked for their origin: "Yep, only Joe could have sent this API request because was signed with his key."
Second, and perhaps more important, any user, say Mary, that wishes to use class2day must themselves indicate to
Yik Yak that they will allow Joe's app to set their status. This step is performed through the Yik Yak app
itself and, similarly, Mary can use the Yik Yak app to revoke a previous authorization that she granted to class2day.
Neither of these choices can be affected by class2day or Joe in any way--it's Mary's call.
To get things going right away at a hackathon, Yik Yak has a procedure to expedite both of the steps above. Talk to the Yik Yak point person about these two things:
1)We have a set of "pre-approved" application ids and application keys that can just be issued to hackathon participants. This avoids the time-consuming approval process.
2)We can pre-authorize Yakkers for use with a particular application. In other words, if three team members Amanda, Barney, and Chris are building an app, we can authorize all three of their production Yik Yak accounts to be able to work with the application they are building. Naturally, this connection is made via the application ID issued in #1!
We'll assume you have an application ID and key (see #1 above) already, and some number of pre-approved users. Each
user is represented with a token that will look like this: cf68a02a-180e-4c52-b72b-92a7a3e1e360
. The application
id will be a six character string (representing a 32bit integer) like this, rMj38Q
and the application key is a
hex-encoded bunch of bits like this 01babf359dd5bfac3c9b4bcc025641e2
.
When you want to set a user's status (and, optionally location) you send a JSON-encoded POST to this URL https://api.yikyak.io/status/v1beta1/setstatus
. Here is an example message, with the "Checksum" field omitted for clarity:
{
"UserToken":"cf68a02a-180e-4c52-b72b-92a7a3e1e360",
"ApplicationId":"rMj38Q",
"Status":"YOLO",
"Latitude":41.0814,
"Longitude":-81.519,
"IgnoreLatLng":true,
"VelocityEsitmateMagnitude":0,
"VelocityEsitmateTheta":0
}
The Checksum field, which would normally be included in the example above, is how Yik Yak can be sure that the POST
message it has received was actually generated by the application associated with the id rMj38Q
. Yik Yak's third
party API uses JSON Web Tokens. (That website has libraries for creating JWTs in many languages.)
To create a JSON web token, you basically need all the data that are planning to send (such as the above) and your
application key. The resulting token effectively encodes two things, who created the token (by virtue of having
the correct key) and what the data was. In principle, one could not bother posting the data to the Yik Yak
status api, and send only the JWT since it encodes all the what was sent. We have chosen to send the data
in addition to the token (we do cross check that they are the same data!) because it makes the whole system easier
to debug when things go wrong at the cost of a few extra bytes.
Here the example above but including the JWT token in the Checksum field.
Note that without knowing the (secret) Application Key used to create the token, it's not much use to anyone. Also
note that the Checksum field itself is not included in the content used to create the Token!
{
"UserToken":"cf68a02a-180e-4c52-b72b-92a7a3e1e360",
"ApplicationId":"rMj38Q",
"Status":"feelDaBern",
"Latitude":41.0814,
"Longitude":-81.519,
"IgnoreLatLng":true,
"VelocityEsitmateMagnitude":0,
"VelocityEsitmateTheta":0,
"Checksum":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJBcHBsaWNhdGlvbklkIjoick1qMzhRIiwiQ2hlY2tzdW0iOiIiLCJJZ25vcmVMYXRMbmciOiJ0cnVlIiwiTGF0aXR1ZGUiOiI0MS4wODE0IiwiTG9uZ2l0dWRlIjoiLTgxLjUxOSIsIlN0YXR1cyI6IllPTE8iLCJVc2VyVG9rZW4iOiJjZjY4YTAyYS0xODBlLTRjNTItYjcyYi05MmE3YTNlMWUzNjAiLCJWZWxvY2l0eUVzaXRtYXRlTWFnbml0dWRlIjoiMCIsIlZlbG9jaXR5RXNpdG1hdGVUaGV0YSI6IjAifQ.QMpNMXiu0IZpC6bB8Yo8_oOns_uJzDajQMuXQ-Fx5bCzWGn_ytpu5vsgsRJKMJ7gy6-Kw_2zKpY6UGfIYOLSrg"
}
When creating the JWT a couple of important details.
- Fields names are case-sensitive and must match the above exactly.
- All fields are compulsory. The two Velocity fields are currently unused, but must be present. You must set Latitude and Longitude (they can be 0.0) even if you have set IgnoreLatLng to true.
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/rsa"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"time"
jwtimpl "github.com/dgrijalva/jwt-go"
)
var (
errNotStruct = errors.New("Not a structure")
errBadFieldType = errors.New("Not a field type that we can flatten")
)
// setStatusRequest request is the actual encoded structure you send to the server
// UserToken is issued by YikYak and represents a user.
// ApplicationId is the string associated with your app, issued by YikYak
// You must supply lat, long, and the two velocities even if you don't actually
// use them (e.g IgnoreLatLng is true). Status should be no more than 18 characters
// (unicode ok) and cannot have spaces. Checksum is computed by the helper
// function jwtIssueTokenForPacket with your secret key (issued by YikYak)
type setStatusRequest struct {
UserToken string
ApplicationID string `json:"ApplicationId"` //make linter happy
Status string
Latitude float64
Longitude float64
IgnoreLatLng bool
VelocityEsitmateMagnitude float64 //ignored but have to be there
VelocityEsitmateTheta float64 //ignored but have to be there
Checksum string
}
func main() {
//this simulates an HTTP request through the gateway
ssr := setStatusRequest{
UserToken: "cf68a02a-180e-4c52-b72b-92a7a3e1e360",
ApplicationID: "rMj38Q",
Status: "feelDaBern",
Latitude: 41.0814,
Longitude: -81.5190,
IgnoreLatLng: true,
VelocityEsitmateMagnitude: 0,
VelocityEsitmateTheta: 0,
}
//the secret key is connected to your application id
secretKey := "01babf359dd5bfac3c9b4bcc025641e2"
checksum, err := jwtIssueTokenForPacket(secretKey, &ssr)
if err != nil {
fmt.Printf("unable to get a checksum for packet: %v", err)
return
}
//you compute the checksum using all the fields of the structure (ssr) *except*
//the checksum field
ssr.Checksum = checksum
//This is the endpoint you must POST to and send the data (see above) as a
//json encoded blob in the body.
urlString := "https://api.yikyak.io/status/v1beta1/setstatus"
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if encodingErr := enc.Encode(&ssr); encodingErr != nil {
fmt.Printf("unable to encode parameters: %v", err)
return
}
fmt.Printf("encoded message to be sent to server\n\n%s\n\n", buf.String())
//you should label the content/type as application/json when you send
resp, err := http.Post(urlString, "application/json", &buf)
if err != nil {
fmt.Printf("unable to post parameters to api: %v", err)
return
}
defer resp.Body.Close() //be careful to close body when done
if resp.StatusCode/100 != 2 {
fmt.Printf("error response: %s\n", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("unable to read the body after response: %v\n", err)
return
}
fmt.Printf("server response (body)\n\n%s\n\n", string(body)) //convert from bytes to string
}
func jwtIssueTokenForPacket(key string, x interface{}) (string, error) {
//get the key from the record
k, err := hex.DecodeString(key)
if err != nil {
return "", err
}
tokenizer := NewJWT(AlgHMAC512, k)
data, err := structToArgsForJWT(x)
if err != nil {
return "", err
}
token, err := tokenizer.IssueNonExpiring(data)
if err != nil {
return "", err
}
return token, nil
}
//structToArgsForJWT takes the structure given in the first arg and returns
//a map[string]string as the first return val. If it can't figure out how
//to convert a field to a string, it returns an error. If the value passed
//as x is not a struct or pointer to a struct, it returns an error.
func structToArgsForJWT(x interface{}) (map[string]string, error) {
v := reflect.ValueOf(x)
if v.Type().Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, errNotStruct
}
result := make(map[string]string)
for i := 0; i < v.Type().NumField(); i++ {
f := v.Type().Field(i)
//check that f is a plausible type
switch f.Type.Kind() {
case reflect.String, reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Float32, reflect.Float64, reflect.Int8, reflect.Bool:
default:
return nil, errBadFieldType
}
//extract the actual field value
field := v.FieldByName(f.Name)
name := f.Name
//this is a horrible hack to deal with our linter at yikyak
if strings.HasSuffix(name, "ID") {
name = strings.TrimSuffix(name, "ID") + "Id"
}
//convert to entry in the result
switch f.Type.Kind() {
case reflect.String:
result[name] = field.String()
case reflect.Float32, reflect.Float64:
result[name] = fmt.Sprintf("%v", field.Float())
case reflect.Bool:
result[name] = fmt.Sprintf("%v", field.Bool())
default: //we've already checked the types above
result[name] = fmt.Sprintf("%v", field.Int())
}
}
return result, nil
}
//
// All the code below has just been inserted here to make it simpler to use this
// program as a single, self-contained file.
//
// Alg specifies a signing method type
type Alg int
const (
// AlgRSA based signing
AlgRSA = iota
// AlgHMAC512 based signing
AlgHMAC512 = iota
// AlgECDSA based signing
AlgECDSA = iota
// AlgHMAC256 based signing
AlgHMAC256 = iota
)
const (
// EXPIRE defines the key used into the claim data for the expiration date
EXPIRE string = "exp"
)
// JWT encapsulates JWT operations
type JWT struct {
algorithm Alg
key interface{}
}
// NewJWT creates a new JWT structure
func NewJWT(algorithm Alg, key interface{}) *JWT {
return &JWT{algorithm, key}
}
func (jwt *JWT) getAlgorithm() (jwtimpl.SigningMethod, error) {
switch jwt.algorithm {
case AlgRSA:
return jwtimpl.SigningMethodRS512, nil
case AlgHMAC512:
return jwtimpl.SigningMethodHS512, nil
case AlgHMAC256:
return jwtimpl.SigningMethodHS256, nil
case AlgECDSA:
return jwtimpl.SigningMethodES256, nil
default:
return nil, errors.New("Unrecognized algorithm")
}
}
// Verify allows the verification of tokens
func (jwt *JWT) Verify(unverifiedToken string) (*map[string]string, error) {
token, err := jwtimpl.Parse(unverifiedToken, func(token *jwtimpl.Token) (interface{}, error) {
// Checks if the token signing method is equal to the one specified by our verifier
alg, err := jwt.getAlgorithm()
if err != nil {
return nil, err
}
if alg != token.Method {
return nil, fmt.Errorf("Unsupported signing method: %v", token.Method)
}
switch alg {
case jwtimpl.SigningMethodRS512:
return &jwt.key.(*rsa.PrivateKey).PublicKey, nil
case jwtimpl.SigningMethodES256:
return &jwt.key.(*ecdsa.PrivateKey).PublicKey, nil
}
return jwt.key, nil
})
// Reject invalid signature
if err != nil || !token.Valid {
return nil, err
}
// Check if token is expired
if exp, ok := token.Claims[EXPIRE]; ok {
if time.Now().Unix() >= int64(exp.(float64)) {
return nil, errors.New("Expired token")
}
}
data := make(map[string]string)
for key, value := range token.Claims {
if s, ok := value.(string); ok {
data[key] = s
}
}
return &data, nil
}
func (jwt *JWT) createToken() *jwtimpl.Token {
alg, err := jwt.getAlgorithm()
if err != nil {
panic(err)
}
return jwtimpl.New(alg)
}
// IssueNonExpiring issues a token without an expiration time
func (jwt *JWT) IssueNonExpiring(data map[string]string) (string, error) {
token := jwt.createToken()
for k := range data {
token.Claims[k] = data[k]
}
// Sign and get the complete encoded token as a string
return token.SignedString(jwt.key)
}
// Issue is used to issue a new token
func (jwt *JWT) Issue(data map[string]string, expireins time.Duration) (string, error) {
token := jwt.createToken()
for k := range data {
token.Claims[k] = data[k]
}
token.Claims["exp"] = time.Now().Add(expireins).Unix()
// Sign and get the complete encoded token as a string
return token.SignedString(jwt.key)
}