ref: 49ba85bf6b67c33707cd56111d9868e610c429ae
dir: /masto9.c/
#include <u.h> #include <libc.h> #include <stdio.h> #include <json.h> #include <auth.h> #define Contenttype "contenttype application/json" typedef struct Attachment { char *type; char *url; } Attachment; typedef struct Notification { char *id; char *type; char *username; char *content; } Notification; typedef struct Toot { char *id; char *content; char *username; char *display_name; char *avatar_url; char *in_reply_to_account_id; int reblogged; char *reblogged_username; Attachment media_attachments[10]; int attachments_count; } Toot; enum { BUFSIZE = 8182, MAX_URL = 1024, TOOTS_COUNT = 20, NOTIFS_COUNT = 15 }; char *URL = "https://fedi.9til.de/api/v1/timelines/home"; char *POSTURL = "https://fedi.9til.de/api/v1/statuses"; char *NOTIFICATIONSURL = "https://fedi.9til.de/api/v1/notifications"; Toot toots[TOOTS_COUNT]; Notification notifs[TOOTS_COUNT]; UserPasswd * getcredentials(char *host) { UserPasswd* p; p = auth_getuserpasswd(auth_getkey, "proto=pass service=mastodon server=%s", host); if(p == nil) sysfatal("mastofs: failed to retrieve token: %r"); return p; } char * concat(char *s1, char *s2) { char *result; result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for the null-terminator if (result == nil) sysfatal("malloc: %r"); strcpy(result, s1); strcat(result, s2); return result; } static void httpget(char *token, char *url, char *body, int bufsize) { int ctlfd, bodyfd, conn, n; char buf[1024]; char *bearer_token; ctlfd = open("/mnt/web/clone", ORDWR); if (ctlfd < 0) sysfatal("open: %r"); n = read(ctlfd, buf, sizeof(buf)); if (n < 0) sysfatal("read: %r"); buf[n] = 0; conn = atoi(buf); /* the write(2) syscall (used by fprint) is considered * failed if it returns -1 or N != Nwritten. to check for * error here you'd have to snprint the url to a temporary * buffer, get its length, then write it out and check the * amount written against the length */ if (fprint(ctlfd, "url %s", url) <= 0) sysfatal("write ctl failed 'url %s': %r", url); bearer_token = concat("Authorization: Bearer ", token); if (fprint(ctlfd, "headers %s", bearer_token) <= 0) sysfatal("write ctl failed 'headers'"); snprint(buf, sizeof(buf), "/mnt/web/%d/body", conn); bodyfd = open(buf, OREAD); if (bodyfd < 0) sysfatal("open %s: %r", buf); if (readn(bodyfd, body, bufsize) <= 0) sysfatal("readn: %r"); close(bodyfd); close(ctlfd); } static void httppost(char *token, char *url, char *response, int bufsize, char *text) { int ctlfd, bodyfd, conn, n; char buf[1024]; char *bearer_token; ctlfd = open("/mnt/web/clone", ORDWR); if (ctlfd < 0) sysfatal("open: %r"); /* n = write(ctlfd, Contenttype, sizeof(Contenttype)); */ /* if (n < 0) */ /* sysfatal("write: %r"); */ buf[n] = 0; conn = atoi(buf); /* the write(2) syscall (used by fprint) is considered * failed if it returns -1 or N != Nwritten. to check for * error here you'd have to snprint the url to a temporary * buffer, get its length, then write it out and check the * amount written against the length */ if (fprint(ctlfd, "url %s", url) <= 0) sysfatal("write ctl failed 'url %s': %r", url); bearer_token = concat("Authorization: Bearer ", token); if (fprint(ctlfd, "headers %s", bearer_token) <= 0) sysfatal("write ctl failed 'headers'"); snprint(buf, sizeof(buf), "/mnt/web/%d/postbody", conn); bodyfd = open(buf, OWRITE); if (bodyfd < 0) sysfatal("open %s: %r", buf); if (write(bodyfd, text, strlen(text)) < 0) sysfatal("write: %r"); close(bodyfd); snprint(buf, sizeof(buf), "/mnt/web/%d/body", conn); bodyfd = open(buf, OREAD); if (bodyfd < 0) sysfatal("open %s: %r", buf); if (readn(bodyfd, buf, sizeof(buf)) <= 0) sysfatal("readn: %r"); /* print("BUF %s", buf); */ close(bodyfd); close(ctlfd); } static JSON * get_json_key(JSON *obj, char *key) { JSON *value = jsonbyname(obj, key); if (value == nil) sysfatal("jsonbyname: key %s not found in %J", key, obj); return value; } void get_timeline(char *token, Toot toots[]) { JSON *obj, *id, *content, *reblog_content, *account, *reblog_account, *username, *reblog_username, *display_name, *avatar, *reblog, *media_attachments, *type, *url; char buf[BUFSIZE * 32]; int i = 0; httpget(token, URL, buf, sizeof(buf)); obj = jsonparse(buf); //print("%J", obj); if (obj == nil) sysfatal("jsonparse: not json"); if (obj->t != JSONArray) sysfatal("jsonparse: not an array"); for(JSONEl *p = obj->first; p != nil; p = p->next) { JSON *toot_json = p->val; id = get_json_key(toot_json, "id"); content = get_json_key(toot_json, "content"); account = get_json_key(toot_json, "account"); username = get_json_key(account, "username"); display_name = get_json_key(account, "display_name"); avatar = get_json_key(account, "avatar_static"); reblog = get_json_key(toot_json, "reblog"); media_attachments = get_json_key(toot_json, "media_attachments"); Toot toot = malloc(sizeof(Toot)); toot.id = strdup((char *)id->s); if(reblog->s == nil) { toot.reblogged = 0; toot.content = strdup((char *)content->s); } else { //print("reblog: %J", toot_json); reblog_content = get_json_key(reblog, "content"); reblog_account = get_json_key(reblog, "account"); reblog_username = get_json_key(reblog_account, "username"); toot.content = strdup((char *)reblog_content->s); toot.reblogged = 1; toot.reblogged_username = strdup((char *)reblog_username->s); media_attachments = get_json_key(reblog, "media_attachments"); }; toot.username = strdup((char *)username->s); toot.display_name = strdup((char *)display_name->s); toot.avatar_url = strdup((char *)avatar->s); toot.attachments_count = 0; if(media_attachments->s != nil) { int j = 0; for(JSONEl *at = media_attachments->first; at != nil; at = at->next) { JSON *attachment_json = at->val; //print("att: %J", attachment_json); Attachment attachment = malloc(sizeof(Attachment)); type = get_json_key(attachment_json, "type"); url = get_json_key(attachment_json, "preview_url"); attachment.type = strdup((char *)type->s); attachment.url = strdup((char *)url->s); toot.media_attachments[j] = attachment; toot.attachments_count++; j++; } } toots[i] = toot; i++; } jsonfree(obj); } void get_notifications(char *token, Notification toots[]) { JSON *obj, *id, *content, *username, *type, *account, *status; char buf[BUFSIZE * 32]; int i = 0; httpget(token, NOTIFICATIONSURL, buf, sizeof(buf)); obj = jsonparse(buf); //print("%J", obj); if (obj == nil) sysfatal("jsonparse: not json"); if (obj->t != JSONArray) sysfatal("jsonparse: not an array"); for(JSONEl *p = obj->first; p != nil; p = p->next) { JSON *toot_json = p->val; id = get_json_key(toot_json, "id"); type = get_json_key(toot_json, "type"); if(strcmp(type->s, "follow") != 0) { status = get_json_key(toot_json, "status"); content = get_json_key(status, "content"); } account = get_json_key(toot_json, "account"); username = get_json_key(account, "username"); Notification toot = malloc(sizeof(Notification)); toot.id = strdup((char *)id->s); toot.type = strdup((char *)type->s); toot.content = strdup((char *)content->s); toot.username = strdup((char *)username->s); toots[i] = toot; i++; } jsonfree(obj); } void post_toot(char *token, char *text) { char buf[BUFSIZE * 32]; httppost(token, POSTURL, buf, sizeof(buf), text); print("RESPONSE %s", buf); } void fav_toot(char *token, char *id) { char buf[BUFSIZE * 32]; char * url = concat(POSTURL, "/"); url = concat(url, id); url = concat(url, "/favourite"); print("URL %s", url); httppost(token, url, buf, sizeof(buf), ""); print("RESPONSE %s", buf); } char * fmthtml(char *msg) { int wr[2], rd[2], n; char buf[BUFSIZE]; if(pipe(wr) == -1 || pipe(rd) == -1) sysfatal("pipe: %r"); switch(fork()){ case -1: sysfatal("fork: %r"); break; case 0: close(wr[0]); close(rd[1]); dup(wr[1], 0); dup(rd[0], 1); execl("/bin/htmlfmt", "htmlfmt -cutf-8 -j", nil); sysfatal("exec: %r"); break; default: close(wr[1]); close(rd[0]); write(wr[0], msg, strlen(msg)); close(wr[0]); n = readn(rd[1], buf, sizeof(buf)); close(rd[1]); if(n == -1) sysfatal("read: %r\n"); buf[n] = 0; return buf; } return buf; } void remove_substring(char *str, char *sub) { int len = strlen(sub); while ((str = strstr(str, sub))) { memmove(str, str + len, strlen(str + len) + 1); } } void remove_tag(char *str, char *tag) { char *start = strstr(str, tag); while (start) { char *end = strchr(start, '>'); if (end) { memmove(start, end + 1, strlen(end + 1) + 1); } else { *start = '\0'; break; } start = strstr(start, tag); } } char * cleanup(char *str) { remove_tag(str, "<span"); remove_substring(str, "</span>"); return str; } /* static void* */ /* slurp(int fd, int *n) */ /* { */ /* char *b; */ /* int r, sz; */ /* *n = 0; */ /* sz = 32; */ /* if((b = malloc(sz)) == nil) */ /* abort(); */ /* while(1){ */ /* if(*n + 1 == sz){ */ /* sz *= 2; */ /* if((b = realloc(b, sz)) == nil) */ /* abort(); */ /* } */ /* r = read(fd, b + *n, sz - *n - 1); */ /* if(r == 0) */ /* break; */ /* if(r == -1){ */ /* free(b); */ /* return nil; */ /* } */ /* *n += r; */ /* } */ /* b[*n] = 0; */ /* return b; */ /* } */ /* static int */ /* webopen(char *url, char *token, char *dir, int ndir) */ /* { */ /* char buf[16]; */ /* int n, cfd, conn; */ /* if((cfd = open("/mnt/web/clone", ORDWR)) == -1) */ /* return -1; */ /* if((n = read(cfd, buf, sizeof(buf)-1)) == -1) */ /* return -1; */ /* buf[n] = 0; */ /* conn = atoi(buf); */ /* bearer_token = concat("Authorization: Bearer ", token); */ /* if(fprint(cfd, "headers %s", bearer_token) <= 0) */ /* goto Error; */ /* if(fprint(cfd, "url %s", url) == -1) */ /* goto Error; */ /* snprint(dir, ndir, "/mnt/web/%d", conn); */ /* return cfd; */ /* Error: */ /* close(cfd); */ /* return -1; */ /* } */ /* static char* */ /* get(char *url, char *token, int *n) */ /* { */ /* char *r, dir[64], path[80]; */ /* int cfd, dfd; */ /* r = nil; */ /* dfd = -1; */ /* if((cfd = webopen(url, token, dir, sizeof(dir))) == -1) */ /* goto Error; */ /* snprint(path, sizeof(path), "%s/%s", dir, "body"); */ /* if((dfd = open(path, OREAD)) == -1) */ /* goto Error; */ /* r = slurp(dfd, n); */ /* Error: */ /* if(dfd != -1) close(dfd); */ /* if(cfd != -1) close(cfd); */ /* return r; */ /* } */ /* static char* */ /* post(char *url, char *buf, int nbuf, int *nret, Hdr *h) */ /* { */ /* char *r, dir[64], path[80]; */ /* int cfd, dfd, hfd, ok; */ /* r = nil; */ /* ok = 0; */ /* dfd = -1; */ /* if((cfd = webopen(url, dir, sizeof(dir))) == -1) */ /* goto Error; */ /* if(write(cfd, Contenttype, strlen(Contenttype)) == -1) */ /* goto Error; */ /* snprint(path, sizeof(path), "%s/%s", dir, "postbody"); */ /* if((dfd = open(path, OWRITE)) == -1) */ /* goto Error; */ /* if(write(dfd, buf, nbuf) != nbuf) */ /* goto Error; */ /* close(dfd); */ /* snprint(path, sizeof(path), "%s/%s", dir, "body"); */ /* if((dfd = open(path, OREAD)) == -1) */ /* goto Error; */ /* if((r = slurp(dfd, nret)) == nil) */ /* goto Error; */ /* if(h != nil){ */ /* snprint(path, sizeof(path), "%s/%s", dir, h->name); */ /* if((hfd = open(path, OREAD)) == -1) */ /* goto Error; */ /* if((h->val = slurp(hfd, &h->nval)) == nil) */ /* goto Error; */ /* close(hfd); */ /* } */ /* ok = 1; */ /* Error: */ /* if(dfd != -1) close(dfd); */ /* if(cfd != -1) close(cfd); */ /* if(!ok && h != nil) */ /* free(h->val); */ /* return r; */ /* } */ JSON * mastodonget(char *token, char *host, char *endpoint) { JSON *obj; char buf[BUFSIZE], url[MAX_URL]; snprintf(url, MAX_URL, "https://%s/api/v1/%s", host, endpoint); httpget(token, url, buf, sizeof(buf)); obj = jsonparse(buf); //print("%J", obj); if (obj == nil) sysfatal("jsonparse: not json"); if (obj->t != JSONArray) sysfatal("jsonparse: not an array"); return(obj); } void usage(void) { sysfatal("usage: masto9 url"); } //echo 'proto=pass service=mastodon server=fedi.9til.de pass=aStT5pM-8RThsxzlZ140YKFZLDmIXSbn4Y6cbIpntDg user=julienxx' > /mnt/factotum/ctl void main(int argc, char**argv) { UserPasswd *p; char *token, *server, *command, *text, *id; if(argc < 2) usage(); JSONfmtinstall(); server = argv[1]; command = argv[2]; p = getcredentials(server); token = p->passwd; if(command == nil) { get_timeline(token, toots); for (int i=0;i<TOOTS_COUNT;i++) { Toot toot = toots[i]; print("\n\n——————————————————————————————————————————————————\n"); if(toot.reblogged == 1) { print("⊙ %s retooted %s:\n", toot.username, toot.reblogged_username); } else { print("⊙ %s:\n", toot.username); } print("\n%s", fmthtml(cleanup(toot.content))); if(toot.attachments_count>0) { for (int j=0;j<toot.attachments_count;j++) { Attachment attachment = toot.media_attachments[j]; print("\n[%s] %s", attachment.type, attachment.url); } print("\n"); } print("\nReply[%s] | Boost[%s] | Favorite[%s]", toot.id, toot.id, toot.id); } print("\n"); } else if(strcmp(command, "toot") == 0) { text = argv[3]; post_toot(token, text); } else if(strcmp(command, "favorite") == 0) { id = argv[3]; fav_toot(token, id); } else if(strcmp(command, "notifications") == 0) { get_notifications(token, notifs); for (int i=0;i<NOTIFS_COUNT;i++) { Notification notif = notifs[i]; if (strcmp(notif.type, "reblog") == 0) { print("⊙ %s retooted\n %s\n", notif.username, fmthtml(cleanup(notif.content))); } else if (strcmp(notif.type, "favourite") == 0) { print("⊙ %s favorited\n %s\n", notif.username, fmthtml(cleanup(notif.content))); } else if (strcmp(notif.type, "follow") == 0) { print("⊙ %s followed you\n", notif.username); } else if (strcmp(notif.type, "poll") == 0) { print("⊙ %s poll ended\n %s\n", notif.username, fmthtml(cleanup(notif.content))); } } } exits(nil); }