/* [ʞ] tenki.c
* ~ lexi hale <lexi@hale.su>
* > ./make.sh
* © affero general public license
* tenki is a client for the Dark Sky API. it is a
* bail-early stream parser which retrieves a fixed set
* of keys (this set may be easily changed in the code)
* and prints them as a formatted string to stdout. it
* was designed purely as a polybar plugin to display my
* local weather, but it could be just as easily be
* reworked for many other purposes
*
* you need to get an api key from dark sky before you
* compile tenki, and place it in the file apikey - it's
* segregated from the main code this way to allow it to
* be excluded from version control.
*
* the default language of the results is Japanese; you
* can change this by changing the macro ds_lang from
* "ja" to your language code of choice. the ds_exclude
* macro is used to minimize the bandwidth tenki uses by
* instructing the dark sky server not to send those
* fields back; if you want to access information in any
* of them, you need to remove them from ds_exclude.
* delete the #define entirely if you wish to access all
* of the excluded fields.
*
* the function main() contains most of the code you
* will likely wish to alter. it contains a variable
* "paths" which is used to specify which fields of the
* returned json object you wish the parser to extract.
* each path is a "p-string of p-strings" - that is, its
* first character is an integer which specifies the
* depth of the path, and each "directory" is specified
* with a string prefixed with the number of characters
* in it. so the path "a/bc/def" would be encoded
* "\3\1a\2bc\3def" - \3 for the three constituents "a"
* "bc" and "def", \1 for the strlen of "a", \2 for the
* strlen of "bc", etc. shoutout to the C standards
* committee for forcing me to do this horrible thing.
*
* in conclusion, i should have written this in scheme.
**/
#define ds_lang "ja"
#define ds_exclude "minutely,hourly,daily,alerts,flags"
// posix includes
#include <stdlib.h>
#include <stddef.h>
#include <stdint.h>
#include <unistd.h>
// libc includes
#include <stdio.h>
#include <string.h>
// networking includes
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <openssl/ssl.h>
#ifndef ds_apikey
# error missing api key; compile with make.sh
# define ds_apikey "" // tidy up errors
#endif
#ifdef ds_exclude
# define _http "?exclude=" ds_exclude "&"
#else
# define _http "?"
#endif
#define http _http "lang=" ds_lang " HTTP/1.1\r\nHost: api.darksky.net\r\n\r\n"
#define start "GET /forecast/" ds_apikey "/"
#define min(x,y) (x>y?y:x)
#define len(x) (sizeof(x)/sizeof(*x))
typedef enum bool { false, true } bool;
SSL_CTX* sc;
bool head(size_t* len, char* txt, char** body) {
char* hend = strstr(txt, "\r\n\r\n");
if (hend == NULL) return false;
#define ctlhead "\r\nContent-Length:"
char* ctl = strstr(txt, ctlhead);
if (ctl == NULL || ctl > hend) return false;
ctl += len(ctlhead);
*len = 0;
while(*ctl != '\r') {
if (*ctl >= '0' && *ctl <= '9') *len*=10,*len+=*ctl-'0';
++ctl;
}
*len+=(hend-txt)+4;
*body = hend+4;
return true;
}
size_t get(char* resp, size_t max, const char* msg, size_t total, char** body){
int port = 443;
const char* host = "api.darksky.net";
//printf("req: %s\n", msg);
struct hostent* api;
struct sockaddr_in api_addr;
int sockfd, bytes;
size_t sent, recd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) exit(2);
SSL *con = SSL_new(sc);
SSL_set_fd(con, sockfd);
api = gethostbyname(host);
if (api == NULL) exit(3);
memset(&api_addr, 0, sizeof(api_addr));
api_addr.sin_family = AF_INET;
api_addr.sin_port = htons(port);
memcpy(&api_addr.sin_addr.s_addr, api -> h_addr, api -> h_length);
if (connect(sockfd,(struct sockaddr*)&api_addr,sizeof(api_addr)))
exit(4);
int err = SSL_connect(con);
sent = 0;
do {
bytes = SSL_write(con,msg+sent,total-sent);
if (bytes < 0) exit (4);
if (bytes == 0) break;
++sent;
} while (sent < total);
recd = 0;
size_t resplen = max;
bool rhead = false;
do {
bytes = SSL_read(con,resp+recd,min(resplen-recd,15000));
if (bytes < 0) exit (5);
if (bytes == 0) break;
if(!rhead)rhead=head(&resplen, resp, body);
recd += bytes;
} while (recd < resplen - 1);
close(sockfd);
SSL_shutdown(con);
return recd;
}
size_t pos(char* msg, const char* lat, const char* lng) {
char* cur = msg + len(start) - 1;
size_t latl = strlen(lat);
size_t longl = strlen(lng);
strcpy(cur, lat); cur += latl;
*cur++=',';
strcpy(cur, lng); cur += longl;
strcpy(cur, http); cur += len(http);
*cur--=0;
return cur-msg;
}
bool estrc(size_t sz, const char* str1, const char* str2) {
for(size_t i = 0;i<sz;++i) {
if(*str1 == *str2) {
++str1; ++str2;
} else if (*str2 =='\\') {
++str2;
if(*str1 == *str2) {
++str1; ++str2;
} else return false;
} else {
return false;
}
}
return true;
}
typedef struct pstr {
size_t sz;
const char* a;
} pstr;
typedef union jsv {
float f;
pstr s;
} jsv;
/* void printkey(const char* key) {
size_t len = *key++;
for (size_t i = 0; i<len; ++i) {
printf("/%.*s",*key,key+1);
key += *key + 1;
};
printf("\n");
} */
void pplt(const char* str, const char* const end, const char** keys, const char** const keyend, jsv* values) {
// initialize parser state
size_t depth = 0;
const char* fld = NULL;
const char* path[32];
path[0] = NULL;
while(*str++!='{'); //cheat
init: {
if (keys > keyend) return;
if (str > end) return;
/* print_path: {
printf("current path: /");
for (size_t i = 0; i < depth; ++i) {
printf("%.*s/",strchr(path[i],'"')-path[i],path[i]);
}
printf("\n");
printf("current key: ");
printkey(*keys);
} */
parse_char: {
switch(*str++) {
case ' ': case '\t': case '\n':
goto parse_char;
case '"':
path[depth] = str;
goto path_find_quote_end;
case '}':
if (depth == 0) return;
depth--;
while(*str!=',') ++str; ++str;
goto init;
default:
printf("found illegal char %c\n", *(str-1));
exit(6);
}
}
path_find_quote_end: {
switch (*str++) {
case '\\': ++str; goto path_find_quote_end;
case '"': goto read_value;
default: goto path_find_quote_end;
}
}
read_value: {
if (*str++!=':') {
printf("illegal char\n");
exit(6);
}
bool iskey;
const char* key = *keys;
jsv* value;
if (depth + 1 != *key++) {
iskey = false;
goto comp;
} else for (size_t i = 0; i <= depth; ++i) {
if (estrc(*key, key+1, path[i])) {
key += *key + 1;
} else {
iskey = false;
goto comp;
}
}
iskey = true;
value = values;
++keys; ++values;
comp: if (*str == '{') {
++depth; ++str;
goto init;
} else if (*str == '"') {
goto copy_str_value;
} else if (*str == '[') {
goto skip_array;
} else if ((*str >= '0' && *str <= '9') || *str=='-') {
goto copy_int_value;
} else {
printf("illegal character found in value %c\n", *str);
exit(6);
}
copy_str_value: {
fld = ++str;
while (*++str != '"') if (*str == '\\') { ++str; continue; }
if (iskey) {
value -> s.sz = str - fld;
value -> s.a = fld;
}
while (*++str != ',');
++str; goto init;
}
skip_array: {
size_t ard = 0;
skip_loop: switch (*++str) {
case '{': case '[': ++ard; goto skip_loop;
case '"': goto skip_str;
case '}': if (ard == 0) {
printf("bad json\n");
exit(7);
} else { --ard; goto skip_loop; }
case ']': if (ard == 0) {
if (*++str == ',') { ++str; goto init; }
else goto init;
} else { --ard; }
default: goto skip_loop;
}
skip_str: while (*str != '"') if (*str == '\\') { str += 2; continue; } else ++str; goto skip_loop;
}
copy_int_value: {
if (!iskey) {
while(*str++!=',') if (str > end) return;
goto init;
}
value -> f = 0;
bool neg;
if (*str == '-') { neg=true; ++str; } else neg=false;
while(*str >= '0' && *str <= '9') {
value -> f *= 10;
value -> f += *str - '0';
++str;
}
if(*str == '.') {
float fac = 0.1;
float val = 0;
while(*++str >= '0' && *str <= '9') {
val += (((float)(*str - '0')) * fac);
fac *= 0.1;
}
value -> f += val;
}
if(neg) value -> f *= -1;
if (*str == ',') {
++str; goto init;
} else {
printf("illegal character %.*s\n",15,str);
exit(6);
}
}
}
}
}
int main(int argc, char** argv) {
if (argc != 3) exit(1);
SSL_load_error_strings();
SSL_library_init();
sc = SSL_CTX_new(SSLv23_client_method());
char msg[256] = start;
char resp[32368];
char* body;
// name the fields to extract from the json stream
// note: MUST BE LISTED IN ORDER THEY APPEAR
// TODO: performance worth lack of flexibility?
const char* paths[] = {
"\2\x9""currently\7summary",
"\2\x9""currently\xfprecipIntensity",
"\2\x9""currently\xbtemperature",
"\2\x9""currently\x9windspeed",
};
//copy the latitude and longitude into the request
size_t msgsz = pos(msg, argv[1], argv[2]);
//msgsz = size of what we're going to transmit
//for (;;) {
size_t rspsz = get(resp, sizeof(resp), msg, msgsz, &body);
// if paths contains the keys, this contains the values
jsv values[len(paths)];
parse: {
pplt(body, body + (rspsz - (resp - body)), paths, paths+len(paths), values);
pstr summary = values[0].s;
float pcpint = values[1].f,
temp = values[2].f,
windspd = values[3].f;
printf("%.1f° %%{F#ff9bbb}%.*s\n",
summary.sz,
summary.a,
temp);
}
// dark sky's API allows us to make 1000 free requests a day.
// let's try not to get too near that.
// hours in day 24 - minutes in day 24 * 60 = 1440
// so once a minute is almost half again too many
// 1440 / 2 = 720 leaving reasonable refresh time without
// going too near the limit. so we aim for one update
// every two minutes.
// sleep(60 * 2);
// }
return 0;
}