shithub: masto9

Download patch

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