├── .gitignore ├── instanceicon.png ├── test.sh ├── config.example.sh ├── events.sh ├── LICENSE ├── daemon.sh ├── lib ├── util.sh ├── db.sh ├── main.sh ├── users.sh ├── act.sh ├── json.sh ├── http.sh ├── httpd.sh ├── inbox.sh └── meta.sh ├── ctl.sh ├── index.html ├── context.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | db/ 2 | config.sh 3 | -------------------------------------------------------------------------------- /instanceicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velzie/kiki/HEAD/instanceicon.png -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | . ./json.sh 2 | 3 | json\ 4 | .a 1\ 5 | .b 2\ 6 | !object 2\ 7 | .a 1\ 8 | @b 2\ 9 | . 1\ 10 | ! 2\ 11 | .b 1\ 12 | .a 2\ 13 | .c 1 14 | 15 | 16 | json\ 17 | .type "i loadasd" 18 | -------------------------------------------------------------------------------- /config.example.sh: -------------------------------------------------------------------------------- 1 | DOMAIN=kiki.velzie.rip 2 | VERSION=2024.0.1 3 | NODENAME=kiki.sh 4 | NODEDESCRIPTION="fedi server written in bash. why did i do this" 5 | 6 | INSTANCEACTOR=shuid 7 | 8 | MAINTAINERNAME=velzie 9 | MAINTAINEREMAIL=velzie@velzie.rip 10 | 11 | # will help with federation if on but is a bit weird 12 | FOLLOWBACK=0 13 | -------------------------------------------------------------------------------- /events.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this file will get run when users interact with posts from this instance. you might want to customize this file 4 | 5 | 6 | . ./lib/main.sh 7 | 8 | 9 | if [[ $1 == "tag" ]]; then 10 | uid=$2 11 | noteid=$3 12 | tagger=$4 13 | 14 | 15 | actorlookup "$tagger" 16 | 17 | 18 | ./ctl.sh act post "$uid" "@$setAcct :3" "$noteid" 19 | fi 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Non-White-Heterosexual-Male License 2 | 3 | If you are not a white heterosexual male you are permitted to copy, sell and use this work in any manner you choose without need to include any attribution you do not see fit. You are asked as a courtesy to retain this license in any derivatives but you are not required. If you are a white heterosexual male you are provided the same permissions (reuse, modification, resale) but are required to include this license in any documentation and any public facing derivative. You are also required to include attribution to the original author or to an author responsible for redistribution of a derivative. 4 | -------------------------------------------------------------------------------- /daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . ./lib/main.sh 4 | PORT=${PORT:-4206} 5 | 6 | 7 | # bash does not have any mechanism to LISTEN on a port. nmap's netcat is the best non-cheaty solution 8 | 9 | # ncat -k with -c will run the command for every request. this means every time main runs it's in a different process 10 | # inside the shell, stdin will be the request, and stdout will be the response 11 | # we want to keep stdout for debugging, so we redirect it to fd 4, and use fd 3 for the response 12 | # fd 4 of ncat then goes back to stdout so everything works 13 | 14 | ncat -k -l -p "$PORT" -c "bash -c '. ./lib/main.sh; ( do_routes>&4 ) 3>&1'" 4>&1 15 | -------------------------------------------------------------------------------- /lib/util.sh: -------------------------------------------------------------------------------- 1 | #shellcheck shell=bash 2 | 3 | 4 | jsonvalid(){ 5 | jq -e . >/dev/null 2>&1 6 | } 7 | 8 | uuid(){ 9 | tr -dc 'a-f0-9' < /dev/urandom | head -c 8 10 | } 11 | 12 | echosafe() { 13 | printf "%s" "$1" 14 | } 15 | 16 | fromhex() { 17 | xxd -p -r -c999999 18 | } 19 | 20 | tohex() { 21 | xxd -p 22 | } 23 | 24 | 25 | urldecode() { 26 | : "${*//+/ }" 27 | echo -e "${_//%/\\x}" 28 | } 29 | 30 | urlencode() { 31 | local length="${#1}" 32 | for ((i = 0; i < length; i++)); do 33 | local c="${1:i:1}" 34 | case $c in 35 | [a-zA-Z0-9.~_-]) printf "%s" "$c" ;; 36 | *) printf "%%%02X" "'$c" ;; 37 | esac 38 | done 39 | } 40 | -------------------------------------------------------------------------------- /ctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . ./lib/main.sh 4 | 5 | 6 | db_init() { 7 | mkdir -p "$DB_USERS" 8 | mkdir -p "$DB_OBJECTS" 9 | } 10 | 11 | useradd() { 12 | read -r -p "Username: " username 13 | read -r -p "UID: " uid 14 | read -r -p "Name: " name 15 | read -r -p "Summary: " summary 16 | 17 | 18 | dir="$DB_USERS/$uid" 19 | mkdir -p "$dir" 20 | 21 | openssl genpkey -algorithm RSA -out "$dir/privkey.pem" -pkeyopt rsa_keygen_bits:2048 22 | openssl rsa -pubout -in "$dir/privkey.pem" -out "$dir/pubkey.pem" 23 | 24 | 25 | cat >"$dir/info" <"$dir/followers" 33 | :>"$dir/following" 34 | } 35 | 36 | act() { 37 | cmd=$1 38 | shift 39 | 40 | "act_$cmd" "$@" 41 | } 42 | 43 | "$@" 44 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | kiki 6 | 7 | 8 | 9 | 10 | 11 |

kiki.sh - a terrible, horrible experiment

12 | 13 |

i have built a fedi server in bash. this request is being served by 14 | bash. you can federate with this server and it 15 | will work (sometimes)

16 |
17 | 18 |

Accounts

19 | 30 |
31 |

this instance has no frontend - you have to look at the accounts on another instance

32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /context.json: -------------------------------------------------------------------------------- 1 | [ 2 | "https://www.w3.org/ns/activitystreams", 3 | "https://w3id.org/security/v1", 4 | { 5 | "Key": "sec:Key", 6 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", 7 | "sensitive": "as:sensitive", 8 | "Hashtag": "as:Hashtag", 9 | "quoteUrl": "as:quoteUrl", 10 | "fedibird": "http://fedibird.com/ns#", 11 | "quoteUri": "fedibird:quoteUri", 12 | "toot": "http://joinmastodon.org/ns#", 13 | "Emoji": "toot:Emoji", 14 | "featured": "toot:featured", 15 | "discoverable": "toot:discoverable", 16 | "schema": "http://schema.org#", 17 | "PropertyValue": "schema:PropertyValue", 18 | "value": "schema:value", 19 | "misskey": "https://misskey-hub.net/ns#", 20 | "_misskey_content": "misskey:_misskey_content", 21 | "_misskey_quote": "misskey:_misskey_quote", 22 | "_misskey_reaction": "misskey:_misskey_reaction", 23 | "_misskey_votes": "misskey:_misskey_votes", 24 | "_misskey_summary": "misskey:_misskey_summary", 25 | "isCat": "misskey:isCat", 26 | "firefish": "https://joinfirefish.org/ns#", 27 | "speakAsCat": "firefish:speakAsCat", 28 | "sharkey": "https://joinsharkey.org/ns#", 29 | "backgroundUrl": "sharkey:backgroundUrl", 30 | "listenbrainz": "sharkey:listenbrainz", 31 | "vcard": "http://www.w3.org/2006/vcard/ns#", 32 | "Bite": "https://ns.mia.jetzt/as#Bite" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /lib/db.sh: -------------------------------------------------------------------------------- 1 | 2 | DB=./db 3 | DB_USERS=$DB/users 4 | 5 | # remote objects and actors 6 | DB_OBJECTS=$DB/objects 7 | DB_ACTORS=$DB/actors 8 | 9 | 10 | finduser() { 11 | username=$1 12 | for actor in $DB_USERS/*; do 13 | source "$actor/info" 14 | if [ "$setUsername" = "$username" ]; then 15 | echo "$setUid" 16 | return 17 | fi 18 | done 19 | } 20 | 21 | userlookup() { 22 | uid=$1 23 | if [ -f "$DB_USERS/$uid/info" ]; then 24 | source "$DB_USERS/$uid/info" 25 | return 0 26 | else 27 | return 1 28 | fi 29 | } 30 | 31 | actorlookup(){ 32 | local actorurl=$1 33 | local actor 34 | local json 35 | local ourid 36 | 37 | 38 | local found=0 39 | 40 | files=$(shopt -s nullglob dotglob; echo $DB_ACTORS/*) 41 | if (( ${#files} > 0 )); then 42 | for actor in $DB_ACTORS/*; do 43 | if [ "$(<"$actor/url" )" = "$actorurl" ]; then 44 | ourid=$(basename "$actor") 45 | found=1 46 | json=$(<"$actor/actor.json") 47 | fi 48 | done 49 | fi 50 | 51 | if [ "$found" = 0 ]; then 52 | json=$(http_get_signed "$actorurl") 53 | if ! jsonvalid <<<"$json"; then 54 | dbg "$json" 55 | return 1 56 | fi 57 | 58 | ourid=$(uuid) 59 | 60 | mkdir -p "$DB_ACTORS/$ourid" 61 | 62 | echosafe "$json" > "$DB_ACTORS/$ourid/actor.json" 63 | echosafe "$actorurl" > "$DB_ACTORS/$ourid/url" 64 | fi 65 | 66 | 67 | 68 | setOurId="$ourid" 69 | setInbox=$(jq -r '.inbox' <<< "$json") 70 | setOutbox=$(jq -r '.outbox' <<< "$json") 71 | setUrl=$(jq -r '.id' <<< "$json") 72 | 73 | local url=$setUrl 74 | url=${url#*//} 75 | setDomain=${url%%/*} 76 | 77 | setAcct=$(jq -r '.preferredUsername' <<< "$json")@$setDomain 78 | 79 | } 80 | -------------------------------------------------------------------------------- /lib/main.sh: -------------------------------------------------------------------------------- 1 | #shellcheck shell=bash 2 | 3 | 4 | . ./config.sh 5 | 6 | 7 | . ./lib/util.sh 8 | . ./lib/json.sh 9 | 10 | . ./lib/httpd.sh 11 | . ./lib/http.sh 12 | 13 | . ./lib/act.sh 14 | 15 | . ./lib/inbox.sh 16 | . ./lib/db.sh 17 | . ./lib/meta.sh 18 | . ./lib/users.sh 19 | 20 | 21 | DOMAINURL=https://$DOMAIN 22 | 23 | # json-ld context. the context object has to list every namespace we implement because software like iceshrimp.NET will do full json-ld expansion 24 | CONTEXT=$(< ./context.json) 25 | 26 | 27 | do_routes() { 28 | # this *isn't* a daemon - every request spawns a new instance 29 | # a bad response can fuck up federation permanently if you get unlucky - better not to respond 30 | 31 | httpd_init 32 | 33 | httpd_route GET / 'httpd_clear && httpd_sendfile 200 index.html' 34 | 35 | 36 | # node meta 37 | httpd_route GET /.well-known/webfinger req_webfinger 38 | httpd_route GET /.well-known/nodeinfo req_nodeinfo 39 | httpd_route GET /.well-known/host-meta req_hostmeta_xml 40 | httpd_route GET /.well-known/host-meta.json req_hostmeta_json 41 | httpd_route GET /instanceicon 'httpd_clear && httpd_sendfile 200 instanceicon.png' 42 | 43 | httpd_route GET '/nodeinfo/*' req_disaspora_nodeinfo 44 | httpd_route GET /manifest.json req_manifest 45 | 46 | # ap routes 47 | httpd_route POST '/sharedinbox' req_ap_inbox 48 | 49 | # user routes 50 | httpd_route GET '/users/*' 'senduserinfo "${path#*users/}"' 51 | httpd_route GET '/@*' 'senduserinfo "$(finduser "${path#*@}" )"' 52 | httpd_route GET '/banner/*' req_user_banner 53 | httpd_route GET '/pfp/*' req_user_pfp 54 | 55 | httpd_route POST '/inbox/*' req_user_inbox 56 | 57 | 58 | # notes? 59 | httpd_route GET '/notes/*' req_note 60 | 61 | 62 | httpd_handle 63 | } 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KiKi 2 | 3 | a super-tiny multithreaded activitypub compliant microblogging platform, written entirely in bash. 4 | 5 | ## Why? 6 | 7 | why not! also i wanted to learn how activitypub works 8 | 9 | ## You can write servers in bash? 10 | Yes! although there's no support for LISTEN in bash, you can hook up the script to netcat to respond to http requests. see `./daemon.sh` and `./lib/httpd.sh` 11 | 12 | ## Features 13 | 14 | | Feature | Supported? | 15 | | -------------------- | -------------------- | 16 | | Notes (Pub/Sub) | :white_check_mark: | 17 | | Follows | :white_check_mark: | 18 | | mia:Bite | :white_check_mark: | 19 | | Likes | :white_check_mark: | 20 | | Mastodon API | :x: | 21 | | Frontend | :x: | 22 | | Announce | :x: | 23 | | Emoji Reactions | :x: | 24 | 25 | # Installation 26 | you need `openssl`, `jq`, `curl`, and `ncat` installed for this 27 | ``` 28 | git clone https://github.com/velzie/kiki 29 | cd kiki 30 | # edit config.sh to your liking 31 | ./ctl.sh db_init 32 | 33 | # now you need to create the instance actor, used to send signed AP requests. You can fill in anything, just make sure the actor id is the same as what you put in config.sh as INSTANCEACTOR 34 | ./ctl.sh useradd 35 | ``` 36 | 37 | # Usage 38 | 39 | This server has no frontend, nor does it implement any client-to-server APIs. All interactions must happen through editing the "database" or with ctl.sh 40 | 41 | - `./daemon.sh` to start the server 42 | - `./ctl.sh useradd` creates a user, make sure to add "pfp.png" and "banner.png" to `db/users//` 43 | - `./ctl.sh act post "content"` will post a note and send it to followers. It will not currently post Announce activities to foreign instances. 44 | - `./ctl.sh act follow ` 45 | 46 | 47 | Since it's so light, you might want to simply use it to run bots. If you do expose this to the web, keep in mind that bash is the most insecure out of all languages and run it as an isolated user. 48 | 49 | 50 | thank you to shittykopper and harper/blueb for helping me understand the AP protocol 51 | -------------------------------------------------------------------------------- /lib/users.sh: -------------------------------------------------------------------------------- 1 | req_user_banner() { 2 | uid=${path#*banner/} 3 | echo "Banner: $uid" 4 | 5 | httpd_clear 6 | httpd_header "Content-Type" "image/png" 7 | httpd_sendfile 200 "$DB_USERS/$uid/banner.png" 8 | } 9 | 10 | req_user_pfp() { 11 | uid=${path#*pfp/} 12 | echo "PFP: $uid" 13 | 14 | httpd_clear 15 | httpd_header "Content-Type" "image/png" 16 | httpd_sendfile 200 "$DB_USERS/$uid/pfp.png" 17 | } 18 | 19 | 20 | 21 | actorjson() { 22 | uid=$1 23 | 24 | if ! userlookup "$uid"; then 25 | httpd_clear 26 | httpd_send 404 "no such user!" 27 | return 28 | fi 29 | 30 | json\ 31 | .type Person\ 32 | .id "$DOMAINURL/users/$uid"\ 33 | .inbox "$DOMAINURL/inbox/$uid"\ 34 | .outbox "$DOMAINURL/outbox/$uid"\ 35 | .followers "$DOMAINURL/follwers/$uid"\ 36 | .following "$DOMAINURL/following/$uid"\ 37 | .featured "$DOMAINURL/collectionsfeatured/$uid"\ 38 | .sharedInbox "$DOMAINURL/sharedinbox"\ 39 | !endpoints 1\ 40 | .sharedInbox "$DOMAINURL/sharedinbox"\ 41 | .url "$DOMAINURL/@$setUsername"\ 42 | .preferredUsername "$setUsername"\ 43 | .name "$setName"\ 44 | .summary "$setSummary"\ 45 | ._misskey_summary "$setSummary"\ 46 | !icon 4\ 47 | .type Image\ 48 | .url "$DOMAINURL/pfp/$uid"\ 49 | %sensitive false\ 50 | %name null\ 51 | !image 4\ 52 | .type Image\ 53 | .url "$DOMAINURL/banner/$uid"\ 54 | %sensitive false\ 55 | %name null\ 56 | !backgroundUrl 4\ 57 | .type Image\ 58 | .url "$DOMAINURL/banner/$uid"\ 59 | %sensitive false\ 60 | %name null\ 61 | @tag 0\ 62 | %manuallyApprovesFollowers false\ 63 | %discoverable true\ 64 | !publicKey 3\ 65 | .id "$DOMAINURL/users/$uid#main-key"\ 66 | .owner "$DOMAINURL/users/$uid"\ 67 | .publicKeyPem "$(< "$DB_USERS/$uid/pubkey.pem")"$'\n'\ 68 | %isCat true\ 69 | %noindex true\ 70 | %speakAsCat false\ 71 | @attachment 0\ 72 | @alsoKnownAs 0 73 | } 74 | 75 | senduserinfo() { 76 | uid=$1 77 | echo "User: $uid" 78 | 79 | if ! userlookup "$uid"; then 80 | httpd_clear 81 | httpd_send 404 "no such user!" 82 | return 83 | fi 84 | 85 | 86 | httpd_clear 87 | httpd_header "Content-Type" "application/activity+json" 88 | 89 | actor=$(actorjson "$uid") 90 | 91 | # add json-ld context to actor object 92 | actor=$(echosafe "$actor" | jq '.["@context"] = $context' --argjson context "$CONTEXT") 93 | 94 | httpd_send 200 "$actor" 95 | } 96 | -------------------------------------------------------------------------------- /lib/act.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | act_follow() { 4 | uid=$1 5 | actor=$2 6 | 7 | followid=$(uuid) 8 | 9 | if ! actorlookup "$actor"; then 10 | dbg "act_follow: no such actor $actor!" 11 | return 12 | fi 13 | 14 | 15 | http_post_json_signed "$setInbox" "$uid"\ 16 | %@context "$CONTEXT"\ 17 | .id "$DOMAINURL/follows/wtf"\ 18 | .type Follow\ 19 | .actor "$DOMAINURL/users/$uid"\ 20 | .object "$actor" 21 | } 22 | 23 | act_bite() { 24 | uid=$1 25 | actor=$2 26 | note=${3-$2} 27 | 28 | if ! actorlookup "$actor"; then 29 | dbg "act_bite: no such actor $actor!" 30 | return 31 | fi 32 | 33 | biteid=$(uuid) 34 | http_post_json_signed "$setInbox" "$uid"\ 35 | %@context "$CONTEXT"\ 36 | .id "$DOMAINURL/bites/$biteid"\ 37 | .type Bite\ 38 | .actor "$DOMAINURL/users/$uid"\ 39 | .target "$note" 40 | } 41 | 42 | act_accept() { 43 | uid=$1 44 | actor=$2 45 | followid=$3 46 | 47 | if ! actorlookup "$actor"; then 48 | dbg "act_accept: no such actor $actor!" 49 | return 50 | fi 51 | 52 | http_post_json_signed "$setInbox" "$uid"\ 53 | %@context "$CONTEXT"\ 54 | .type Accept\ 55 | .id "$DOMAINURL/accepts/$(uuid)"\ 56 | .actor "$DOMAINURL/users/$uid"\ 57 | !object 4\ 58 | .id "$followid"\ 59 | .actor "$actor"\ 60 | .object "$actor"\ 61 | .type Follow 62 | } 63 | 64 | 65 | act_post() { 66 | uid=$1 67 | content=$2 68 | inreplyto=$3 69 | to=$4 70 | 71 | # first create the note 72 | noteid=$(uuid) 73 | 74 | mkdir -p "$DB_OBJECTS/$noteid" 75 | echo "Note" > "$DB_OBJECTS/$noteid/type" 76 | 77 | json=$(json\ 78 | ._misskey_content "$content"\ 79 | .content "

$content

"\ 80 | .sensitive false\ 81 | .published "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"\ 82 | !source 2\ 83 | .content "$content"\ 84 | .mediaType "text/x.misskeymarkdown"\ 85 | @to 2\ 86 | . "$DOMAINURL/followers/$uid"\ 87 | . "$to"\ 88 | @cc 1\ 89 | . "https://www.w3.org/ns/activitystreams#Public"\ 90 | .attributedTo "$DOMAINURL/users/$uid"\ 91 | @tag 0\ 92 | .id "$DOMAINURL/notes/$noteid"\ 93 | .type Note) 94 | 95 | 96 | if [ -n "$inreplyto" ]; then 97 | json=$(jq --arg inreplyto "$inreplyto" '.inReplyTo = $inreplyto' <<< "$json") 98 | fi 99 | 100 | echosafe "$json" > "$DB_OBJECTS/$noteid/object.json" 101 | 102 | 103 | # now add it to the user's outbox 104 | echo "$noteid" >> "$DB_USERS/$uid/outbox" 105 | 106 | # then send it to our loyal followers 107 | 108 | while read -r follower; do 109 | actorlookup "$follower" 110 | http_post_json_signed "$setInbox" "$uid"\ 111 | %@context "$CONTEXT"\ 112 | .id "$DOMAINURL/notes/$noteid"\ 113 | .type Create\ 114 | .actor "$DOMAINURL/users/$uid"\ 115 | %object "$(< "$DB_OBJECTS/$noteid/object.json")" 116 | done < "$DB_USERS/$uid/followers" 117 | 118 | } 119 | -------------------------------------------------------------------------------- /lib/json.sh: -------------------------------------------------------------------------------- 1 | #shellcheck shell=bash 2 | 3 | 4 | dbg() { 5 | echo "$@" >&2 6 | } 7 | 8 | # .: string 9 | # %: json 10 | # @: array 11 | 12 | 13 | json() { 14 | _json 99999 "$@" 15 | } 16 | 17 | _json() { 18 | local count=$1 19 | shift 20 | 21 | local command=(-r -n) 22 | local shiftby=0 23 | 24 | local j=0 25 | while [ "$j" -lt "$count" ]; do 26 | j=$((j+1)) 27 | 28 | 29 | 30 | 31 | type=${1:0:1} 32 | name=${1:1} 33 | shift 34 | shiftby=$((shiftby+1)) 35 | 36 | if [ "$type" = "." ]; then 37 | value=$1 38 | shift 39 | shiftby=$((shiftby+1)) 40 | 41 | command+=("--arg" "$name" "$value") 42 | elif [ "$type" = "%" ]; then 43 | value=$1 44 | shift 45 | shiftby=$((shiftby+1)) 46 | 47 | command+=("--argjson" "$name" "$value") 48 | elif [ "$type" = "@" ]; then 49 | arrlen=$1 50 | shift 51 | shiftby=$((shiftby+1)) 52 | 53 | _jsonr "$arrlen" "$@" 54 | _shiftby=$? 55 | 56 | for ((i=0; i<_shiftby; i++)); do 57 | shift 58 | shiftby=$((shiftby+1)) 59 | done 60 | 61 | command+=("--argjson" "$name" "$out") 62 | 63 | elif [ "$type" = "!" ]; then 64 | local numfields=$1 65 | shift 66 | 67 | shiftby=$((shiftby+1)) 68 | 69 | 70 | out=$(_json "$numfields" "$@") 71 | 72 | _shiftby=$? 73 | 74 | local k 75 | for ((k=0; k<_shiftby; k++)); do 76 | shift 77 | shiftby=$((shiftby+1)) 78 | done 79 | 80 | command+=("--argjson" "$name" "$out") 81 | else 82 | dbg "Invalid type: $type" 83 | return 1 84 | fi 85 | 86 | if [ "$#" -eq 0 ]; then 87 | break 88 | fi 89 | done 90 | 91 | command+=('$ARGS.named') 92 | 93 | # dbg "${command[@]}" 94 | 95 | jq "${command[@]}" 96 | 97 | return $shiftby 98 | } 99 | 100 | 101 | _jsonr() { 102 | local count=$1 103 | shift 104 | 105 | local shiftby=0 106 | out="[" 107 | 108 | 109 | for ((i=0; i&3 110 | } 111 | 112 | httpd_json() { 113 | status=$1 114 | shift 115 | 116 | httpd_send "$status" "$(json "$@")" 117 | } 118 | 119 | 120 | 121 | httpd_sendfile() { 122 | status=$1 123 | file=$2 124 | 125 | if [ ! -f "$file" ]; then 126 | dbg "warn: sendfile: file not found: $file" 127 | httpd_clear 128 | httpd_send 404 "Not Found" 129 | return 130 | fi 131 | 132 | { 133 | echo -en "HTTP/1.1 $status OK\r\n" 134 | echo -en "Content-Length: $(stat -c %s "$file")\r\n" 135 | echo -en "Content-Type: $(file -b --mime-type "$file")\r\n" 136 | for key in "${!G_headers[@]}"; do 137 | echo -en "$key: ${G_headers[$key]}\r\n" 138 | done 139 | echo -en "\r\n" 140 | cat "$file" 141 | } >&3 142 | } 143 | 144 | 145 | httpd_read() { 146 | length=${G_headers[content-length]} 147 | length=${length:-0} 148 | head -c "$length" 149 | } 150 | -------------------------------------------------------------------------------- /lib/inbox.sh: -------------------------------------------------------------------------------- 1 | 2 | add_object() { 3 | object=$1 4 | ourid=$(uuid) 5 | mkdir -p "$DB_OBJECTS/$ourid" 6 | 7 | type=$(jq -r '.type' <<< "$object") 8 | 9 | echosafe "$type" > "$DB_OBJECTS/$ourid/type" 10 | echosafe "$object" > "$DB_OBJECTS/$ourid/object.json" 11 | 12 | echo "$ourid" 13 | } 14 | 15 | 16 | notelookup(){ 17 | local noteid=$1 18 | 19 | if ! [ -d "$DB_OBJECTS/$noteid/" ]; then 20 | dbg "notelookup: no such note $noteid!" 21 | return 1 22 | fi 23 | 24 | setType=$(<"$DB_OBJECTS/$noteid/type") 25 | setJson=$(<"$DB_OBJECTS/$noteid/object.json") 26 | setOwner=$(jq -r '.attributedTo' <<< "$setJson") 27 | setContent=$(jq -r '.content' <<< "$setJson") 28 | } 29 | 30 | req_note(){ 31 | id=${path#*notes/} 32 | 33 | 34 | if ! notelookup "$id"; then 35 | httpd_clear 36 | httpd_send 404 "no such note!" 37 | return 38 | fi 39 | 40 | 41 | httpd_clear 42 | httpd_header "Content-Type" "application/activity+json" 43 | 44 | # add context to the note json 45 | setJson=$(echosafe "$setJson" | jq '.["@context"] = $context' --argjson context "$CONTEXT") 46 | 47 | httpd_send 200 "$setJson" 48 | } 49 | 50 | 51 | 52 | in_act_follow() { 53 | local json=$1 54 | local followid 55 | local actor 56 | local object 57 | local uid 58 | 59 | 60 | echo "INBOX FOLLOW $json" 61 | 62 | followid=$(jq -r '.id' <<< "$json") 63 | actor=$(jq -r '.actor' <<< "$json") 64 | object=$(jq -r '.object' <<< "$json") 65 | # actor is the remote actor, object is our actor 66 | 67 | uid=${object#*users/} 68 | 69 | if ! userlookup "$uid"; then 70 | httpd_clear 71 | httpd_send 404 "no such user!" 72 | return 73 | fi 74 | 75 | echo "FOLLOW REQUEST: $actor -> $object" 76 | echo "FOLLOW ID: $followid" 77 | 78 | actorlookup "$actor" 79 | 80 | act_accept "$uid" "$actor" "$followid" 81 | 82 | echo "$actor" >> "$DB_USERS/$uid/followers" 83 | 84 | 85 | if [ -n "$FOLLOWBACK" ]; then 86 | act_follow "$uid" "$actor" 87 | fi 88 | } 89 | 90 | req_user_inbox() { 91 | uid=${path#*inbox/} 92 | 93 | json=$(httpd_read) 94 | type=$(jq -r '.type' <<< "$json") 95 | 96 | inbox 97 | } 98 | 99 | req_user_outbox() { 100 | uid=$1 101 | echo "Outbox: $uid" 102 | 103 | 104 | if ! userlookup "$uid"; then 105 | httpd_clear 106 | httpd_send 404 "no such user!" 107 | return 108 | fi 109 | 110 | numactivities=$(find "$DB_USERS/$uid/activities" -type f | wc -l) 111 | 112 | httpd_clear 113 | httpd_header "Content-Type" "application/activity+json" 114 | httpd_json 200\ 115 | %@context "$CONTEXT"\ 116 | .id "$DOMAINURL/outbox/$uid"\ 117 | .type OrderedCollection\ 118 | %totalItems $numactivities\ 119 | .first "$DOMAINURL/outbox/$uid?page=true"\ 120 | .last "$DOMAINURL/outbox/$uid?page=true?since_id=0"\ 121 | 122 | 123 | } 124 | 125 | 126 | req_ap_inbox() { 127 | json=$(httpd_read) 128 | type=$(jq -r '.type' <<< "$json") 129 | 130 | inbox 131 | } 132 | 133 | inbox() { 134 | 135 | if [[ "$type" = "Create" ]]; then 136 | add_object "$(jq -r '.object' <<< "$json")" 137 | noteurl=$(jq -r '.object.id' <<< "$json") 138 | 139 | attributedTo=$(jq -r '.object.attributedTo' <<< "$json") 140 | 141 | tags=$(jq -r '.object.tag | map (.href) | join (" ")' <<< "$json") 142 | for tag in $tags; do 143 | if [[ "$tag" = "$DOMAINURL/users"* ]]; then 144 | uid=${tag#*users/} 145 | echo "$uid was tagged!!" 146 | ./events.sh tag "$uid" "$noteurl" "$attributedTo" 147 | fi 148 | done 149 | 150 | elif [[ "$type" = "Follow" ]]; then 151 | in_act_follow "$json" 152 | elif [[ "$type" = "Like" ]]; then 153 | object=$(jq -r '.object' <<< "$json") 154 | actor=$(jq -r '.actor' <<< "$json") 155 | 156 | noteid=${object#*notes/} 157 | echo "LIKE $noteid" 158 | echosafe "$actor" >> "$DB_OBJECTS/$noteid/likes" 159 | elif [[ "$type" = "Accept" ]]; then 160 | : 161 | elif [[ "$type" = "Delete" ]]; then 162 | # don't care. spams the logs 163 | : 164 | else 165 | echo "UNKNOWN ACTIVITY: $type" 166 | echo "$json" 167 | fi 168 | 169 | 170 | httpd_clear 171 | httpd_send 200 172 | } 173 | -------------------------------------------------------------------------------- /lib/meta.sh: -------------------------------------------------------------------------------- 1 | req_manifest(){ 2 | httpd_clear 3 | httpd_header "Content-Type" "application/json" 4 | httpd_json 200\ 5 | .short_name "$NODENAME"\ 6 | .name "$NODENAME"\ 7 | .start_url "/"\ 8 | .display "standalone"\ 9 | .description "$NODEDESCRIPTION"\ 10 | .background_color "#00ae00"\ 11 | .theme_color "#00ae00"\ 12 | .icons 1\ 13 | ! 3\ 14 | .src "$DOMAINURL/instanceicon"\ 15 | .sizes "512x512"\ 16 | .type "image/png"\ 17 | !share_target 4\ 18 | .action "/share/"\ 19 | .method "GET"\ 20 | .enctype "application/x-www-form-urlencoded"\ 21 | !params 3\ 22 | .title "title"\ 23 | .text "text"\ 24 | .url "url" 25 | } 26 | 27 | req_hostmeta_xml(){ 28 | httpd_clear 29 | httpd_header "Content-Type" "application/xrd+xml" 30 | httpd_send 200\ 31 | "$(cat < 33 | 34 | 35 | 36 | EOF 37 | )" 38 | } 39 | 40 | req_hostmeta_json() { 41 | httpd_clear 42 | httpd_header "Content-Type" "application/json" 43 | httpd_json 200\ 44 | @links 1\ 45 | ! 3\ 46 | .rel "lrdd"\ 47 | .type "application/json"\ 48 | .template "$DOMAINURL/.well-known/webfinger?resource={uri}" 49 | } 50 | 51 | req_webfinger() { 52 | resource=${G_search[resource]} 53 | echo "Webfinger: $resource" 54 | 55 | 56 | if [[ "$resource" = "acct:"* ]]; then 57 | account=${resource#*acct:} 58 | 59 | username=${account%@*} 60 | 61 | uid=$(finduser "$username") 62 | else 63 | # iceshrimp.net resource 64 | uid=${resource#*users/} 65 | fi 66 | 67 | if ! userlookup "$uid"; then 68 | httpd_clear 69 | httpd_send 404 "no such user!" 70 | return 71 | fi 72 | 73 | 74 | 75 | 76 | httpd_clear 77 | httpd_header "Content-Type" "application/jrd+json" 78 | httpd_json 200\ 79 | .subject "acct:$setUsername@$DOMAIN"\ 80 | @links 3\ 81 | ! 3\ 82 | .rel self\ 83 | .type "application/activity+json"\ 84 | .href "$DOMAINURL/users/$uid"\ 85 | ! 3\ 86 | .rel "http://webfinger.net/rel/profile-page"\ 87 | .type "text/html"\ 88 | .href "$DOMAINURL/@$setUsername"\ 89 | ! 2\ 90 | .rel "http://ostatus.org/schema/1.0/subscribe"\ 91 | .template "$DOMAINURL/authorize-follow?acct={uri}" 92 | 93 | } 94 | 95 | req_nodeinfo() { 96 | httpd_clear 97 | httpd_header "Content-Type" "application/json" 98 | httpd_json 200\ 99 | @links 2\ 100 | ! 2\ 101 | .rel "http://nodeinfo.diaspora.software/ns/schema/2.1"\ 102 | .href "$DOMAINURL/nodeinfo/2.1"\ 103 | ! 2\ 104 | .rel "http://nodeinfo.diaspora.software/ns/schema/2.0"\ 105 | .href "$DOMAINURL/nodeinfo/2.0"\ 106 | 107 | } 108 | 109 | req_disaspora_nodeinfo() { 110 | httpd_header "Content-Type" "application/json" 111 | httpd_json 200\ 112 | .version 2.1\ 113 | !software 3\ 114 | .name kiki\ 115 | .version "$VERSION"\ 116 | .homepage "$HOMEPAGE"\ 117 | @protocols 1\ 118 | . activitypub\ 119 | !services 1\ 120 | @inbound 0\ 121 | @outbound 0\ 122 | %openRegistrations false\ 123 | !usage 3\ 124 | !users 3\ 125 | %total 1\ 126 | %activeHalfyear null\ 127 | %activeMonth null\ 128 | %localPosts 10\ 129 | %localComments 0\ 130 | !metadata 28\ 131 | .nodeName "$NODENAME"\ 132 | .nodeDescription "$NODEDESCRIPTION"\ 133 | @nodeAdmins 0\ 134 | !maintainer 2\ 135 | .name "$MAINTAINERNAME"\ 136 | .email "$MAINTAINEREMAIL"\ 137 | @langs 0\ 138 | %tosUrl null\ 139 | %privacyPolicyUrl null\ 140 | .inquiryUrl "https://www.youtube.com/watch?v=K8GQcE-XwK0"\ 141 | %impressumUrl null\ 142 | %donationUrl null\ 143 | .repositoryUrl "https://github.com/velzie/kiki"\ 144 | %feedbackUrl null\ 145 | %disableRegistration true\ 146 | %disableLocalTimeline false\ 147 | %disableGlobalTimeline false\ 148 | %disableBubbleTimeline false\ 149 | %emailRequiredForSignup true\ 150 | %enableHcaptcha false\ 151 | %enableRecaptcha false\ 152 | %enableMcaptcha false\ 153 | %enableTurnstile false\ 154 | %maxNoteTextLength 8192\ 155 | %enableEmail false\ 156 | %enableServiceWorker false\ 157 | %proxyAccountName null\ 158 | .themeColor "#00ae00" 159 | 160 | 161 | } 162 | 163 | --------------------------------------------------------------------------------