# Smithereen 1.0.3: walls, friends as mutual follows, threaded comments, likes, reposts, polls, private messages, edits # and deletions, a post on someone else's wall, groups, the unfollow, statistics. Smithereen is driven through its # VKontakte-like API (/api/method/, v=1.0) as smuser, with the token peers/smithereen.sh got by a password grant. SM=https://smithereen.test:6443 SMT=$(cat "$here/.state/smithereen/smuser.token" 2>/dev/null) SFT=$(cat "$here/.state/smithereen/smfriend.token" 2>/dev/null) # sm [name=value ...]: an API call as smuser (SMT names whose token), printed as Smithereen answers it sm() { local method=$1; shift local args=() for kv in "$@"; do args+=(--data-urlencode "$kv"); done site smithereen.test -s -X POST "$SM/api/method/$method" -H "Authorization: Bearer ${SMTOKEN:-$SMT}" --data-urlencode v=1.0 "${args[@]}" } smf() { SMTOKEN=$SFT sm "$@"; } sm_sql() { podman exec pasture-mysql mysql -uroot -ppasture -N smithereen -e "$1" 2>/dev/null; } # sm_load : what Smithereen makes of it, as "type id" sm_load() { sm utils.loadRemoteObject "q=$1" | j "r = d.get('response') or {}; print(r.get('type', ''), r.get('id', ''))"; } p_home_has() { curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next(((s.get('reblog') or s)['id'] for s in d if '$1' in ((s.get('reblog') or s)['content'] or '')), ''))"; } p_status() { curl -s -H "$PH" "$P/api/v1/statuses/$1" | j "print(d.get('$2'))"; } echo "smithereen" [ "$(sm users.get fields=screen_name | j "print(d['response'][0]['screen_name'])")" = "smuser" ] && ok "Smithereen token for smuser" || { ko "Smithereen token"; return 1; } PT=$(privapub_token alice_smithereen) PH="Authorization: Bearer $PT" [ -n "$PT" ] && ok "PrivaPub token for alice_smithereen" || { ko "PrivaPub token for alice_smithereen"; return 1; } alice_uri="$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d['url'])" | sed 's|/@|/peasants/|')" run=$(date +%s) sm_self=$(sm users.get | j "print(d['response'][0]['id'])") echo " friends" read -r kind alice_on_sm <<< "$(sm_load "$alice_uri")" [ "$kind" = "user" ] && ok "Smithereen resolves alice as a user" || ko "Smithereen cannot resolve alice ($kind)" sm friends.add "user_id=$alice_on_sm" >/dev/null until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d[\"followers_count\"])")" -ge 1 ]' \ && ok "smuser follows alice" || ko "Smithereen's follow never reached alice" sm_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=smuser@smithereen.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") [ -n "$sm_on_p" ] && ok "PrivaPub resolves smuser" || ko "PrivaPub cannot resolve smuser" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$sm_on_p/follow" until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$sm_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \ && ok "alice follows smuser (Accept arrived)" || ko "smuser's Accept never arrived" until_true 30 '[ "$(sm friends.areFriends "user_ids=$alice_on_sm" | j "print(d[\"response\"][0][\"friend_status\"])")" = "friends" ]' \ && ok "the two follows make them friends on Smithereen" || ko "Smithereen does not count them as friends ($(sm friends.areFriends "user_ids=$alice_on_sm"))" # (alice's profile, told again: Smithereen learns of her wall, and that PrivaPub's actors have walls, when it reads her) curl -s -o /dev/null -X PATCH -H "$PH" "$P/api/v1/accounts/update_credentials" -d "note=alice of the pasture" until_true 30 '[ -n "$(sm_sql "select 1 from servers where host='"'privapub.test'"' and features & 1")" ]' \ && ok "Smithereen learns that PrivaPub's actors have walls" || ko "Smithereen never took PrivaPub for a server with walls" echo " walls" sm_post=$(sm wall.post "message=a Smithereen wall post $run" | j "print(d['response'])") until_true 45 '[ -n "$(p_home_has "a Smithereen wall post $run")" ]' && ok "smuser's wall post reaches alice's home" || ko "smuser's wall post never reached alice" sm_post_on_p=$(p_home_has "a Smithereen wall post $run") p_post=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d "status=a PrivaPub post for Smithereen $run&visibility=public") p_post_id=$(echo "$p_post" | j "print(d['id'])"); p_post_uri=$(echo "$p_post" | j "print(d['uri'])") until_true 45 '[ "$(sm_sql "select count(*) from wall_posts where ap_id='"'$p_post_uri'"'")" = "1" ]' \ && ok "alice's post reaches Smithereen" || ko "alice's post never reached Smithereen" p_post_on_sm=$(sm_sql "select id from wall_posts where ap_id='$p_post_uri'") echo " comments" sm wall.createComment "post_id=$p_post_on_sm" "message=a Smithereen comment $run" >/dev/null until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id/context" | j "print(any(\"a Smithereen comment $run\" in s[\"content\"] for s in d[\"descendants\"]))")" = "True" ]' \ && ok "smuser's comment threads under alice's post" || ko "smuser's comment missing on PrivaPub" curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=a PrivaPub comment $run&in_reply_to_id=$sm_post_on_p&visibility=public" until_true 45 '[ "$(sm wall.getComments "post_id=$sm_post" | j "print(any(\"a PrivaPub comment $run\" in (c.get(\"text\") or \"\") for c in d[\"response\"][\"items\"]))")" = "True" ]' \ && ok "alice's reply becomes a comment on smuser's post" || ko "alice's reply never reached Smithereen ($(sm wall.getComments "post_id=$sm_post" | head -c 300))" echo " likes and reposts" sm likes.add type=post "item_id=$p_post_on_sm" >/dev/null until_true 45 '[ "$(p_status $p_post_id favourites_count)" = "1" ]' && ok "smuser's like counts on PrivaPub" || ko "smuser's like never counted" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$sm_post_on_p/favourite" until_true 45 '[ "$(sm_sql "select count(*) from likes where object_id=$sm_post and object_type=0")" = "1" ]' \ && ok "alice's favourite is a like on Smithereen" || ko "alice's favourite never counted on Smithereen" # a repost there is a quote: a QuoteRequest to alice, which her policy grants, then the quoting post (its API fails # without a message, even an empty one) sm wall.repost "post_id=$p_post_on_sm" message= >/dev/null until_true 45 '[ "$(p_status $p_post_id quotes_count)" = "1" ]' && ok "smuser's repost quotes alice's post, which lets it" || ko "smuser's repost never counted as a quote" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$sm_post_on_p/reblog" until_true 45 '[ "$(sm_sql "select count(*) from wall_posts where repost_of=$sm_post")" = "1" ]' \ && ok "alice's boost is a repost on Smithereen" || ko "alice's boost never reached Smithereen" echo " polls" poll=$(sm polls.create "question=A Smithereen poll $run" 'answers=["hay","clover"]' | j "print(d['response'])") sm wall.post "message=vote $run" "attachments=[{\"type\":\"poll\",\"poll_id\":$poll}]" >/dev/null until_true 45 '[ -n "$(p_home_has "vote $run")" ]' && ok "smuser's poll reaches alice" || ko "smuser's poll never reached alice" poll_on_p=$(p_home_has "vote $run") p_poll=$(curl -s -H "$PH" "$P/api/v1/statuses/$poll_on_p" | j "print(d['poll']['id'] if d.get('poll') else '')") [ -n "$p_poll" ] && ok "as a poll" || ko "the poll arrived without its question" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/polls/$p_poll/votes" -d 'choices[]=1' until_true 45 '[ "$(sm_sql "select count(*) from poll_votes where poll_id=$poll")" = "1" ]' \ && ok "alice's vote counts on Smithereen" || ko "alice's vote never counted on Smithereen" echo " edits and deletions" sm wall.edit "post_id=$sm_post" "message=a Smithereen wall post $run, edited" >/dev/null until_true 45 '[ "$(p_status $sm_post_on_p content | grep -c "edited")" = "1" ]' && ok "smuser's edit reaches PrivaPub" || ko "smuser's edit never reached PrivaPub" curl -s -o /dev/null -X PUT -H "$PH" "$P/api/v1/statuses/$p_post_id" -d "status=a PrivaPub post for Smithereen $run, edited" until_true 45 '[ "$(sm_sql "select text like '"'%edited%'"' from wall_posts where id=$p_post_on_sm")" = "1" ]' \ && ok "alice's edit reaches Smithereen" || ko "alice's edit never reached Smithereen" gone=$(sm wall.post "message=a Smithereen post to delete $run" | j "print(d['response'])") until_true 45 '[ -n "$(p_home_has "a Smithereen post to delete $run")" ]' gone_on_p=$(p_home_has "a Smithereen post to delete $run") sm wall.delete "post_id=$gone" >/dev/null until_true 45 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$gone_on_p")" = "404" ]' \ && ok "smuser's deletion reaches PrivaPub" || ko "smuser's deleted post still shows on PrivaPub" curl -s -o /dev/null -X DELETE -H "$PH" "$P/api/v1/statuses/$p_post_id" until_true 45 '[ "$(sm_sql "select count(*) from wall_posts where ap_id='"'$p_post_uri'"'")" = "0" ]' \ && ok "alice's deletion reaches Smithereen" || ko "alice's deleted post still on Smithereen" echo " someone else's wall" # smfriend writes on smuser's wall: the post carries the wall as its target, and smuser tells its followers' servers # with Add{Note}, which is no pin read -r _ sm_on_friend <<< "$(SMTOKEN=$SFT sm_load "https://smithereen.test/users/$sm_self")" smf wall.post "owner_id=$sm_self" "message=a word on the wall of smuser $run" >/dev/null # (Smithereen sends a post on someone else's wall, and its owner's Add, only to servers whose actors have a wall: G-0009) until_true 45 '[ -n "$(p_home_has "a word on the wall of smuser $run")" ]' && ok "a post on smuser's wall reaches alice" \ || ko "a post on smuser's wall never reaches alice" on_wall=$(p_home_has "a word on the wall of smuser $run") [ "$(curl -s -H "$PH" "$P/api/v1/statuses/$on_wall" | j "print((d['privapub'].get('wall') or {}).get('acct'))")" = "smuser@smithereen.test" ] \ && ok "as written on smuser's wall" || ko "PrivaPub does not say it is on smuser's wall" [ "$(curl -s -H "$PH" "$P/api/v1/accounts/$sm_on_p/statuses?pinned=true" | j "print(len(d))")" = "0" ] \ && ok "and is not taken for a pin" || ko "PrivaPub took the wall's Add for a pin" echo " alice's wall" # smuser follows alice, so it may write on her wall (her actor says her followers may): PrivaPub hosts the post, tells # alice, lists it on her wall and tells her followers' servers with Add; alice takes it off, which deletes it on Smithereen sm_wall_post=$(sm wall.post "owner_id=$alice_on_sm" "message=a word on alice's wall $run" | j "print(d.get('response', ''))") [ -n "$sm_wall_post" ] && ok "Smithereen lets smuser write on alice's wall" || ko "Smithereen refused smuser's post on alice's wall ($(sm wall.post "owner_id=$alice_on_sm" "message=x" | head -c 200))" p_mention() { curl -s -H "$PH" "$P/api/v1/notifications?types[]=mention&limit=40" | j "print(next((n['status']['id'] for n in d if 'a word on alice' in n['status']['content'] and '$run' in n['status']['content']), ''))"; } until_true 45 '[ -n "$(p_mention)" ]' && ok "the post on alice's wall reaches her" || ko "the post on alice's wall never reached her" walled=$(p_mention) [ "$(curl -s -H "$PH" "$P/api/v1/statuses/$walled" | j "print((d['privapub'].get('wall') or {}).get('username'))")" = "alice_smithereen" ] \ && ok "as written on her wall" || ko "PrivaPub does not say it is on alice's wall" walled_uri=$(curl -s -H "$PH" "$P/api/v1/statuses/$walled" | j "print(d['uri'])") pfetch -s -H 'Accept: application/activity+json' "$alice_uri/graffiti?page=true" | grep -q "$walled_uri" \ && ok "her wall lists it" || ko "her wall does not list it" # (the Add delivered to Smithereen, done: Smithereen holds the post already, its author's; the body's quotes are \u0022) p_adds_done() { podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'print(db.Job.countDocuments({Host:"smithereen.test", State:2, Payload:/type\\u0022:\\u0022Add\\u0022.*graffiti/}))'; } until_true 30 '[ "$(p_adds_done)" -ge 1 ]' && ok "alice tells her followers' servers with Add" || ko "no Add reached Smithereen" curl -s -o /dev/null -X DELETE -H "$PH" "$P/api/v1/statuses/$walled" until_true 45 '[ "$(sm_sql "select count(*) from wall_posts where id=$sm_wall_post")" = "0" ]' \ && ok "alice takes it off her wall, and Smithereen deletes it" || ko "the post is still on Smithereen after alice took it off" [ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$walled")" = "404" ] && ok "and it is gone here" || ko "it is still here" echo " private messages" sm messages.send "to=$alice_on_sm" "body=a Smithereen message $run" >/dev/null until_true 45 'curl -s -H "$PH" "$P/api/v1/conversations" | grep -q "a Smithereen message $run"' && ok "smuser's message arrives as a DM" || ko "smuser's message never reached alice" dm=$(curl -s -H "$PH" "$P/api/v1/conversations" | j "print(next(c['last_status']['id'] for c in d if 'a Smithereen message $run' in c['last_status']['content']))") curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=@smuser@smithereen.test a PrivaPub answer $run&in_reply_to_id=$dm&visibility=direct" until_true 45 '[ "$(sm_sql "select count(*) from mail_messages where text like '"'%a PrivaPub answer $run%'"'")" -ge 1 ]' \ && ok "alice's answer arrives as a Smithereen message" || ko "alice's answer never reached Smithereen" echo " unfollow" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$sm_on_p/unfollow" # smuser still follows alice: no longer friends until_true 45 '[ "$(sm friends.areFriends "user_ids=$alice_on_sm" | j "print(d[\"response\"][0][\"friend_status\"])")" = "following" ]' \ && ok "alice's unfollow reaches Smithereen" || ko "Smithereen still counts alice ($(sm friends.areFriends "user_ids=$alice_on_sm"))" echo " statistics" stats_check smithereen.test smithereen