util  tenki.c at tip

File tenki/tenki.c from the latest check-in


/* [ʞ] 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;
}