ref: ef2a51a97c36d1f279993eb17314fa4b080fe488
author: glenda <glenda@cirno>
date: Thu Feb 20 14:30:55 EST 2025
initial
--- /dev/null
+++ b/9volt
@@ -1,0 +1,113 @@
+#!/bin/rc
+# Made by Rosa Green ("rosie"), licensed under BSD
+# LONG LIVE 9FRONT
+APIVER=0.8
+
+fn sendtextmsg{
+ awk 'BEGIN{printf "t %0.20d%s", length(ARGV[1]), ARGV[1]}' $1 >[1=0]
+}
+fn readheader{
+ header=`{read -c 22}
+ length=`{echo $header(2) p | dc}
+ echo $header(1) $length
+}
+fn handlemsg{
+ echo Handling message:
+ echo ' TYPE :' $1
+ echo ' LENGTH:' $2
+ echo ' BODY :' $3
+}
+
+fn render {
+ message=$1
+ author=''
+ for(user in `{ls /mnt/json/users/}) {
+ if(~ `{cat $user/_id} `{cat $message/author}) {
+ author=`{cat $user/username}
+ if(test -e $user/display_name)
+ author=`{cat $user/display_name}
+
+ for(member in `{ls /mnt/json/members/}) {
+ if(~ `{cat $member/_id/user} `{cat $message/author} \
+ && test -e $member/nickname)
+ author=`{cat $member/nickname}
+ }
+ }
+ }
+ if(test -e $message/replies){
+ for(reply in `{ls $message/replies}) {
+ for(messagetest in `{ls /mnt/json/messages/}) {
+ if(~ `{cat $messagetest/_id} `{cat $reply}) {
+ content='<No message content>'
+ if(test -e $message/content)
+ content=`{cat $messagetest/content}
+ echo '>' $content
+ }
+ }
+ }
+ }
+ content='<No message content>'
+ if(test -e $message/content)
+ content=`{cat $message/content}
+ echo $author: $content
+}
+
+fn load {
+ channelid=$1
+ token=$2
+ hget -r 'x-session-token: '$token https://api.revolt.chat/$APIVER/channels/$channelid'/messages?include_users' | jsonfs
+ for(message in `{seq 49 -1 0}) {
+ render /mnt/json/messages/$message
+ }
+}
+
+fn websocket {
+ <>/mnt/web/clone {
+ d=/mnt/web/^`{sed 1q}
+ echo url wss://echo.websocket.org >[1=0]
+ <>$d/body {
+ read -n 1
+ sendtextmsg 'ping'
+ while(header=`{readheader}){
+ echo Header: $header
+ body=`{read -c $header(2)}
+ handlemsg $header(1) $header(2) $"body
+ sleep 2
+ sendtextmsg 'ping'
+ }
+ }
+ }
+}
+fn connect {
+ token=`{auth/userpasswd 'service=retaped-cli'}
+ token=$token(2)
+ load $1 $token
+ websocket&
+ while() {
+ input=`{read}
+ hget -m POST -r 'x-session-token: '$token -p '{"content":"'$"input'"}' https://api.revolt.chat/$APIVER/channels/$1/messages > /dev/null
+ }
+}
+
+fn login {
+ hget -m POST -p '{"email":"'$1'","password":"'$2'"}' https://api.revolt.chat/$APIVER/auth/session/login | jsonfs
+ echo 'key proto=pass service=retaped-cli user=idk !password='`{cat /mnt/json/token} > /mnt/factotum/ctl
+}
+
+fn help {
+ echo '9volt: CLI Revolt client for Plan9(front)'
+ echo 'Usage: 9volt [-lch] [serverID|email] [channelID|password]'
+ echo '-l email password - Login using provided credentials'
+ echo '-c serverID channelID - Connect to Revolt and filter for the specified server and channel; token is loaded from Factotum'
+}
+
+switch ($1) {
+ case -l
+ login $2 $3
+ case -c
+ connect $2
+ case -h
+ help
+ case *
+ help
+}
--- /dev/null
+++ b/jsonfs.c
@@ -1,0 +1,184 @@
+#include <u.h>
+#include <libc.h>
+#include <auth.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include <json.h>
+#include <bio.h>
+#include <String.h>
+
+static char *delim = nil;
+static char *uid = "json";
+static char *mtpt = nil;
+
+void
+fsread(Req *r)
+{
+ if(r->fid->file->aux)
+ readstr(r, r->fid->file->aux);
+ respond(r, nil);
+}
+
+Srv fs = {
+ .read = fsread,
+};
+
+char*
+esmprint(char *fmt, ...)
+{
+ char *s;
+ va_list arg;
+
+ va_start(arg, fmt);
+ s = vsmprint(fmt, arg);
+ va_end(arg);
+ if(s == nil)
+ sysfatal("out of memory");
+ return s;
+}
+
+char*
+jprint(char *fmt, ...)
+{
+ char *s, *p;
+ va_list arg;
+
+ p = nil;
+ if(delim != nil){
+ if((p = smprint("%s%s", fmt, delim)) == nil)
+ sysfatal("out of memory");
+ fmt = p;
+ }
+
+ va_start(arg, fmt);
+ s = vsmprint(fmt, arg);
+ va_end(arg);
+ if(s == nil)
+ sysfatal("out of memory");
+
+ if(p)
+ free(p);
+
+ return s;
+}
+
+void
+json2files(File *dir, JSON *j)
+{
+ File *d, *f;
+ JSONEl *tok;
+ char *p;
+ void *s;
+ int anum = 0;
+ int bnum = 0;
+
+ for(tok = j->first; tok != nil; tok = tok->next){
+ s = nil;
+
+ switch(tok->val->t){
+ case JSONNull:
+ s = jprint("null");
+ break;
+ case JSONBool:
+ if(tok->val->n)
+ s = jprint("true");
+ else
+ s = jprint("false");
+ break;
+ case JSONNumber:
+ s = jprint("%0.f", tok->val->n);
+ break;
+ case JSONString:
+ s = jprint("%s", tok->val->s);
+ break;
+ case JSONArray:
+ case JSONObject:
+ if((s = tok->name) == nil || strlen(s) < 1)
+ s = esmprint("%d", anum++);
+
+ if((d = createfile(dir, s, uid, DMDIR|0777, nil)) == nil)
+ sysfatal("createfile: JSONObject: %r");
+ d->qid.type = QTDIR;
+ json2files(d, tok->val);
+ continue;
+ default:
+ sysfatal("wut: %r");
+ }
+ if((p = tok->name) == nil)
+ p = esmprint("%d", bnum++);
+ if((f = createfile(dir, p, uid, 0666, s)) != nil)
+ f->length = strlen(s);
+ }
+}
+
+void
+populatetree(File *root, int fd)
+{
+ Biobuf in;
+ String *s;
+ JSON *j;
+
+ Binit(&in, fd, OREAD);
+ s = s_new();
+
+ while(s_read(&in, s, 8192))
+ ;
+
+ if((j = jsonparse(s_to_c(s))) == nil)
+ sysfatal("jsonparse: %r");
+
+ json2files(root, j);
+
+ s_free(s);
+ Bterm(&in);
+}
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s [-D] [-s srv] [-m mtpt] [-n] [file]\n", argv0);
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ char *srv;
+ int fd;
+
+ fd = 0;
+ srv = nil;
+ mtpt = "/mnt/json";
+ ARGBEGIN{
+ case 'D':
+ chatty9p++;
+ break;
+ case 's':
+ srv = EARGF(usage());
+ break;
+ case 'm':
+ mtpt = EARGF(usage());
+ break;
+ case 'n':
+ delim = "\n";
+ break;
+ default:
+ usage();
+ }ARGEND;
+ if(argc > 1)
+ usage();
+ else if(argc == 1){
+ if((fd = open(argv[0], OREAD)) < 0)
+ sysfatal("open: %r");
+ }
+
+ if(chatty9p)
+ fprint(2, "srvname %s mtpt %s\n", srv, mtpt);
+
+ fs.tree = alloctree(nil, nil, DMDIR|0777, nil);
+ populatetree(fs.tree->root, fd);
+ postmountsrv(&fs, srv, mtpt, MREPL|MCREATE);
+ exits(nil);
+}
+