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