ref: 49ba85bf6b67c33707cd56111d9868e610c429ae
author: Julien Blanchard <julien@typed-hole.org>
date: Wed Oct 7 07:12:38 EDT 2020
Initial commit: display latest toots
--- /dev/null
+++ b/masto9.c
@@ -1,0 +1,596 @@
+#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);
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,17 @@
+</$objtype/mkfile
+
+TARG=masto9
+
+BIN=/$objtype/bin
+
+OFILES=\
+ masto9.$O\
+
+UPDATE=\
+ $HFILES\
+ ${OFILES:%.$O=%.c}\+ mkfile\
+
+default:V: all
+
+</sys/src/cmd/mkone
--
⑨