diff --git a/ttyverse.pl b/ttyverse.pl index 3fe3f6b..3e2e06f 100755 --- a/ttyverse.pl +++ b/ttyverse.pl @@ -1717,7 +1717,7 @@ exit(0) if (length($status)); if (length($credentials)) { print "-- processing credentials: "; $my_json_ref = &map_mastodon_fields(&parsejson($credentials)); - $whoami = lc($my_json_ref->{'screen_name'}); + $whoami = lc($my_json_ref->{'username'} || $my_json_ref->{'acct'}); if (!length($whoami)) { print "FAILED!\nis your account suspended, or wrong token?\n"; exit; @@ -2239,7 +2239,7 @@ print $stdout "*** invalid UTF-8: partial delete of a wide character?\n"; my $sn; my $id; my @superfields = ( - [ "user", "screen_name" ], # must always be first + [ "user", "username" ], # must always be first [ "reblog", "id_str" ], [ "user", "geo_enabled" ], [ "place", "id" ], @@ -2293,7 +2293,7 @@ print $stdout "*** invalid UTF-8: partial delete of a wide character?\n"; my $sn; my $id; my @superfields = ( - [ "sender", "screen_name" ], # must always be first + [ "sender", "username" ], # must always be first ); if (!defined($dm)) { @@ -2941,9 +2941,18 @@ EOF print $stdout "-- synchronous /again command for $uname ($countmaybe)\n" if ($verbose); - my $my_json_ref = - &grabjson("${uurl}?screen_name=${uname}&include_rts=true", - 0, 0, $countmaybe, undef, 1); + + # Look up account ID for the username + my $account_id = &lookup_account_id($uname); + if (!$account_id) { + print $stdout "-- ERROR: Could not find account for user: $uname\n"; + return 0; + } + + # Use proper Mastodon API endpoint with account ID + my $user_statuses_url = $uurl; + $user_statuses_url =~ s/%I/$account_id/g; + my $my_json_ref = &grabjson($user_statuses_url, 0, 0, $countmaybe, undef, 1); &dt_tdisplay($my_json_ref, 'again'); unless ($mode eq 'w' || $mode eq 'wf') { return 0; @@ -2955,11 +2964,21 @@ EOF $readline_completion{'@'.$uname}++ if ($termrl); print $stdout "-- synchronous /whois command for $uname\n" if ($verbose); - my $my_json_ref = - &grabjson("${wurl}?screen_name=${uname}", 0, 0, 0, undef, 1); + + # Look up account ID for the username + my $account_id = &lookup_account_id($uname); + if (!$account_id) { + print $stdout "-- ERROR: Could not find account for user: $uname\n"; + return 0; + } + + # Use proper Mastodon API endpoint with account ID + my $user_info_url = $wurl; + $user_info_url =~ s/%I/$account_id/g; + my $my_json_ref = &grabjson($user_info_url, 0, 0, 0, undef, 1); if (defined($my_json_ref) && ref($my_json_ref) eq 'HASH' && - length($my_json_ref->{'screen_name'})) { + length($my_json_ref->{'username'} || $my_json_ref->{'acct'})) { my $sturl = undef; my $purl = &descape($my_json_ref->{'profile_image_url'}); @@ -2999,18 +3018,18 @@ ${EM}Picture:${OFF}\t@{[ &descape($my_json_ref->{'profile_image_url'}) ]} EOF unless ($anonymous || $whoami eq $uname) { - my $g = &grabjson( - "$frurl?source_screen_name=$whoami&target_screen_name=$uname", 0, 0, 0, - undef, 1); - print $streamout &wwrap( - "${EM}Do you follow${OFF} this user? ... ${EM}$g->{'relationship'}->{'target'}->{'followed_by'}${OFF}\n") - if (ref($g) eq 'HASH'); - my $g = &grabjson( - "$frurl?source_screen_name=$uname&target_screen_name=$whoami", 0, 0, 0, - undef, 1); - print $streamout &wwrap( -"${EM}Does this user follow${OFF} you? ... ${EM}$g->{'relationship'}->{'target'}->{'followed_by'}${OFF}\n") - if (ref($g) eq 'HASH'); + # Use Mastodon relationships API with account ID + my $relationship_url = $blockingurl; + $relationship_url =~ s/%I/$account_id/g; + my $g = &grabjson($relationship_url, 0, 0, 0, undef, 1); + + if (ref($g) eq 'ARRAY' && @$g > 0) { + my $rel = $g->[0]; # relationships API returns array + print $streamout &wwrap( + "${EM}Do you follow${OFF} this user? ... ${EM}$rel->{'following'}${OFF}\n"); + print $streamout &wwrap( +"${EM}Does this user follow${OFF} you? ... ${EM}$rel->{'followed_by'}${OFF}\n"); + } print $streamout "\n"; } print $stdout &wwrap( @@ -3036,21 +3055,11 @@ EOF print $stdout "--- sorry, this won't work on lists.\n"; return 0; } - my $g = &grabjson( -"${frurl}?source_screen_name=${user_a}&target_screen_name=${user_b}", 0, 0, 0, - undef, 1); - if ($msg = &is_json_error($g)) { - print $stdout <<"EOF"; -${MAGENTA}*** warning: server error message received -*** "$ec"${OFF} -EOF - } elsif ($g->{'relationship'}->{'target'}) { - print $stdout "--- does $user_a follow ${user_b}? => "; - print $streamout "$g->{'relationship'}->{'target'}->{'followed_by'}\n" - } else { - print $stdout -"-- sorry, bogus server response, try again later.\n"; - } + # Note: Fediverse APIs don't support checking arbitrary user relationships + # You can only check your own relationships via /api/v1/accounts/relationships + print $stdout "-- Sorry, /doesfollow is not supported in fediverse (privacy feature)\n"; + print $stdout "-- You can only check your own relationships with /whois \@username\n"; + return 0; return 0; } @@ -3104,15 +3113,21 @@ EOF return 0; } - # grab all the IDs - my $ids_ref = &grabjson( - "$mode?limit=${countmaybe}&screen_name=${who}", - 0, 0, 0, undef, 1); - return 0 if (!$ids_ref || ref($ids_ref) ne 'HASH' || - !$ids_ref->{'ids'}); - $ids_ref = $ids_ref->{'ids'}; - return 0 if (ref($ids_ref) ne 'ARRAY'); - my @ids = @{ $ids_ref }; + # Look up account ID for the username + my $account_id = &lookup_account_id($who); + if (!$account_id) { + print $stdout "-- ERROR: Could not find account for user: $who\n"; + return 0; + } + + # Use proper Mastodon API endpoint with account ID + my $followers_url = $mode; + $followers_url =~ s/%I/$account_id/g; + my $accounts_ref = &grabjson("$followers_url?limit=${countmaybe}", 0, 0, 0, undef, 1); + return 0 if (!$accounts_ref || ref($accounts_ref) ne 'ARRAY'); + + # Fediverse (Mastodon/Pleroma/etc) returns array of account objects, extract IDs + my @ids = map { $_->{'id'} } @{ $accounts_ref }; @ids = sort { 0+$a <=> 0+$b } @ids; # make it somewhat deterministic @@ -3407,8 +3422,16 @@ EOF $countmaybe += 0; if (length) { - $my_json_ref = &grabjson("${favsurl}?screen_name=$_", - 0, 0, $countmaybe, undef, 1); + # Look up account ID for the username + my $account_id = &lookup_account_id($_); + if (!$account_id) { + print $stdout "-- ERROR: Could not find account for user: $_\n"; + return 0; + } + + # Use fediverse accounts/{id}/favourites endpoint + $my_json_ref = &grabjson("${apibase}/accounts/${account_id}/favourites?limit=${countmaybe}", + 0, 0, 0, undef, 1); } else { if ($anonymous) { print $stdout @@ -3488,7 +3511,7 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) { } # we can't or user requested /ert /ort $repost = "boost @" . - &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'screen_name'}) . + &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'username'}) . ": " . &descape($post->{'text'}); if ($mode eq 'e') { &add_history($repost); @@ -3565,7 +3588,7 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) { print $stdout "-- no such post (yet?): $code\n"; return 0; } - if (lc(&descape($post->{'user'}->{'screen_name'})) + if (lc(&descape($post->{'user'}->{'username'} || $post->{'user'}->{'acct'})) ne lc($whoami)) { print $stdout "-- not allowed to delete somebody's else's posts\n"; @@ -3641,29 +3664,19 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) { print $stdout "-- no such post (yet?): $code\n"; return 0; } - # Use acct field if available, otherwise construct from screen_name and server + # Use Mastodon's acct field (includes @domain for remote users) or username for local users my $target; - if ($post->{'user'}->{'acct'} && $post->{'user'}->{'acct'} =~ /\@/) { - # Full acct field with domain - $target = &descape($post->{'user'}->{'acct'}); - } elsif ($post->{'user'}->{'screen_name'}) { - # If no domain in acct, try to get it from the post URL or fallback to screen_name - my $screen_name = &descape($post->{'user'}->{'screen_name'}); - if ($post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) { - my $domain = $1; - # Don't add domain if it's the same as our server - if ($domain ne $fediverseserver) { - $target = "$screen_name\@$domain"; - } else { - $target = $screen_name; - } - } else { - $target = $screen_name; + my $acct = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'username'}); + $target = $acct; + + # If acct doesn't include @domain and this is a remote post, construct it + if ($acct !~ /\@/ && $post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) { + my $domain = $1; + if ($domain ne $fediverseserver) { + $target = "$acct\@$domain"; } - } else { - $target = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'screen_name'}); } - print $stdout "-- DEBUG: Reply target acct='$post->{'user'}->{'acct'}', screen_name='$post->{'user'}->{'screen_name'}', url='$post->{'url'}', using='$target'\n" if ($verbose); + print $stdout "-- DEBUG: Reply target acct='$post->{'user'}->{'acct'}', username='$post->{'user'}->{'username'}', url='$post->{'url'}', using='$target'\n" if ($verbose); $_ = '@' . $target . " $_"; unless ($mode eq 'v') { $in_reply_to = $post->{'id_str'}; @@ -3708,27 +3721,17 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) { print $stdout "-- no such post (yet?): $code\n"; return 0; } - # Use acct field if available, otherwise construct from screen_name and server + # Use Mastodon's acct field (includes @domain for remote users) or username for local users my $target; - if ($post->{'user'}->{'acct'} && $post->{'user'}->{'acct'} =~ /\@/) { - # Full acct field with domain - $target = &descape($post->{'user'}->{'acct'}); - } elsif ($post->{'user'}->{'screen_name'}) { - # If no domain in acct, try to get it from the post URL or fallback to screen_name - my $screen_name = &descape($post->{'user'}->{'screen_name'}); - if ($post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) { - my $domain = $1; - # Don't add domain if it's the same as our server - if ($domain ne $fediverseserver) { - $target = "$screen_name\@$domain"; - } else { - $target = $screen_name; - } - } else { - $target = $screen_name; + my $acct = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'username'}); + $target = $acct; + + # If acct doesn't include @domain and this is a remote post, construct it + if ($acct !~ /\@/ && $post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) { + my $domain = $1; + if ($domain ne $fediverseserver) { + $target = "$acct\@$domain"; } - } else { - $target = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'screen_name'}); } my $text = $_; $_ = '@' . $target; @@ -4175,11 +4178,11 @@ EOF # ones they subscribe to, different from 1.0. # right now we just deal with that. #next if ($uname ne - # $list_ref->{'user'}->{'screen_name'}); + # $list_ref->{'user'}->{'username'} || $list_ref->{'user'}->{'acct'}); # listhandle? my $list_name = -"\@$list_ref->{'user'}->{'screen_name'}/@{[ &descape($list_ref->{'slug'}) ]}"; +"\@".($list_ref->{'user'}->{'username'} || $list_ref->{'user'}->{'acct'})."/@{[ &descape($list_ref->{'slug'}) ]}"; my $list_full_name = (length($list_ref->{'name'})) ? &descape($list_ref->{'name'})."${OFF} ($list_name)" : $list_name; @@ -4382,7 +4385,7 @@ sub reply_to_all { # Get the post content and author my $post_content = $post_ref->{'text'} || ''; - my $post_author = $post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'screen_name'} || ''; + my $post_author = $post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'username'} || ''; # Set up reply-to ID my $in_reply_to = $post_ref->{'id_str'} || $post_ref->{'id'}; @@ -4801,7 +4804,7 @@ EOF $key->{'tag'}->{'type'}. " ". # NO SPACES! unpack("${pack_magic}H*", $key->{'tag'}->{'payload'}). " ". ($key->{'reblogs_count'} || "0") . " " . - $key->{'user'}->{'screen_name'}." $ds $src|". + ($key->{'user'}->{'username'} || $key->{'user'}->{'acct'})." $ds $src|". unpack("${pack_magic}H*", $key->{'text'}). $space_pad), 0, 1024); print P $key; @@ -5318,7 +5321,7 @@ sub handle_fediverse_stream_event { # this so the user can interpose custom logic. if ($nostreamreplies) { my $sn = &descape( - $payload->{'user'}->{'screen_name'}); + $payload->{'user'}->{'username'} || $payload->{'user'}->{'acct'}); my $text = &descape($payload->{'text'}); return if (&$posttype($payload, $sn, $text) eq 'reply'); @@ -5350,7 +5353,7 @@ sub handle_fediverse_stream_event { elsif (!$notimeline) { $w = $w->{'payload'}; my $sou_sn = - &descape($w->{'source'}->{'screen_name'}); + &descape($w->{'source'}->{'username'} || $w->{'source'}->{'acct'}); if (!length($sou_sn) || !$filterusers_sub || !&$filterusers_sub($sou_sn)) { &send_removereadline if ($termrl); @@ -5381,7 +5384,7 @@ sub handle_mastodon_stream_event { # Filter replies if requested if ($nostreamreplies) { - my $sn = &descape($payload->{'user'}->{'screen_name'}); + my $sn = &descape($payload->{'user'}->{'username'} || $payload->{'user'}->{'acct'}); my $text = &descape($payload->{'text'}); return if (&$posttype($payload, $sn, $text) eq 'reply'); } @@ -5717,7 +5720,7 @@ sub tdisplay { # used by both synchronous /again and asynchronous refreshes my $g = ($i-1); $j = $my_json_ref->[$g]; my $id = $j->{'id_str'}; - my $sn = $j->{'user'}->{'screen_name'}; + my $sn = $j->{'user'}->{'username'} || $j->{'user'}->{'acct'}; next if (!length($sn)); $sn = lc(&descape($sn)); @@ -5748,7 +5751,7 @@ sub tdisplay { # used by both synchronous /again and asynchronous refreshes $filterusers_sub && &$filterusers_sub(lc(&descape($j-> {'reblog'}-> - {'user'}->{'screen_name'})))); + {'user'}->{'username'} || $j->{'user'}->{'acct'})))); # second, filterrts. this is almost as fast. (&killtw($j), next) if @@ -6307,7 +6310,14 @@ sub foruuser { my $basef = shift; my $verb = shift; - my ($en, $em) = ¢ral_cd_dispatch("screen_name=$uname", + # Look up account ID for the username + my $account_id = &lookup_account_id($uname); + if (!$account_id) { + print $stdout "-- ERROR: Could not find account for user: $uname\n" if ($interactive); + return 1; + } + + my ($en, $em) = ¢ral_cd_dispatch("id=$account_id", $interactive, $basef); print $stdout "-- ok, you have $verb following user $uname.\n" if ($interactive && !$en); @@ -6321,7 +6331,14 @@ sub boruuser { my $basef = shift; my $verb = shift; - my ($en, $em) = ¢ral_cd_dispatch("screen_name=$uname", + # Look up account ID for the username + my $account_id = &lookup_account_id($uname); + if (!$account_id) { + print $stdout "-- ERROR: Could not find account for user: $uname\n" if ($interactive); + return 1; + } + + my ($en, $em) = ¢ral_cd_dispatch("id=$account_id", $interactive, $basef); print $stdout "-- ok, you have $verb blocking user $uname.\n" if ($interactive && !$en); @@ -6336,8 +6353,15 @@ sub rtsonoffuser { my $verb = ($selection) ? 'enabled' : 'disabled'; my $tval = ($selection) ? 'true' : 'false'; + # Look up account ID for the username + my $account_id = &lookup_account_id($uname); + if (!$account_id) { + print $stdout "-- ERROR: Could not find account for user: $uname\n" if ($interactive); + return 1; + } + my ($en, $em) = ¢ral_cd_dispatch( - "reposts=${tval}&screen_name=${uname}", + "reposts=${tval}&id=${account_id}", $interactive, $frupdurl); print $stdout "-- ok, you have ${verb} boosts for user $uname.\n" if ($interactive && !$en); @@ -6362,7 +6386,7 @@ sub standardpost { my $ref = shift; my $nocolour = shift; - my $sn = &descape($ref->{'user'}->{'acct'} || $ref->{'user'}->{'screen_name'}); + my $sn = &descape($ref->{'user'}->{'acct'} || $ref->{'user'}->{'username'}); my $post = &descape($ref->{'text'}); # Debug boost display @@ -6683,8 +6707,8 @@ sub standardevent { # ActivityPub streaming API messages if (length($verb)) { # see below for server-level events - my $tar_sn = '@'.&descape($ref->{'target'}->{'acct'} || $ref->{'target'}->{'screen_name'}); - my $sou_sn = '@'.&descape($ref->{'source'}->{'acct'} || $ref->{'source'}->{'screen_name'}); + my $tar_sn = '@'.&descape($ref->{'target'}->{'acct'} || $ref->{'target'}->{'username'}); + my $sou_sn = '@'.&descape($ref->{'source'}->{'acct'} || $ref->{'source'}->{'username'}); my $tar_list_name = ''; my $tar_list_desc = ''; @@ -7030,7 +7054,7 @@ sub defaulthandle { my $post_ref = shift; my $class = shift; my $dclass = ($verbose) ? "{$class,$post_ref->{'id_str'}} " : ''; - my $sn = &descape($post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'screen_name'}); + my $sn = &descape($post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'username'}); my $post = &descape($post_ref->{'text'}); # Debug: Check what data defaulthandle receives for boost posts @@ -7074,7 +7098,7 @@ sub userline { # used by both $userhandle and /whois ($my_json_ref->{'protected'} eq 'true') ? "${EM}(Protected)${OFF} " : ''; print $fh <<"EOF"; -${CCprompt}@{[ &descape($my_json_ref->{'name'}) ]}${OFF} (@{[ &descape($my_json_ref->{'screen_name'}) ]}) (f:$my_json_ref->{'friends_count'}/$my_json_ref->{'followers_count'}) (u:$my_json_ref->{'statuses_count'}) ${verified}${protected} +${CCprompt}@{[ &descape($my_json_ref->{'name'}) ]}${OFF} (@{[ &descape($my_json_ref->{'username'} || $my_json_ref->{'acct'}) ]}) (f:$my_json_ref->{'friends_count'}/$my_json_ref->{'followers_count'}) (u:$my_json_ref->{'statuses_count'}) ${verified}${protected} EOF return; } @@ -7082,7 +7106,7 @@ sub sendnotifies { # this is a default subroutine of a sort, right? my $post_ref = shift; my $class = shift; - my $sn = &descape($post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'screen_name'}); + my $sn = &descape($post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'username'}); my $post = &descape($post_ref->{'text'}); # Debug: Show what we received @@ -7193,6 +7217,33 @@ sub mark_conversation_read { } } +sub lookup_account_id { + my $username = shift; + return unless ($username); + + # Remove @ prefix if present + $username =~ s/^@//; + + print $stdout "-- DEBUG: Looking up account ID for username: $username\n" if ($verbose); + + # Use Mastodon search API to find account + my $search_result = &grabjson("${searchurl}?q=${username}&type=accounts&limit=1", 0, 0, 0, undef, 1); + + if ($search_result && $search_result->{'accounts'} && @{$search_result->{'accounts'}}) { + my $account = $search_result->{'accounts'}->[0]; + my $found_username = $account->{'username'} || $account->{'acct'}; + + # Verify we found the right account (case-insensitive match) + if (lc($found_username) eq lc($username) || lc($account->{'acct'}) eq lc($username)) { + print $stdout "-- DEBUG: Found account ID: " . $account->{'id'} . " for $username\n" if ($verbose); + return $account->{'id'}; + } + } + + print $stdout "-- DEBUG: Could not find account ID for username: $username\n" if ($verbose); + return undef; +} + sub defaulteventhandle { (&flag_default_call, return) if ($multi_module_context); my $event_ref = shift; @@ -8551,17 +8602,17 @@ sub normalizejson { $i->{'reblog'} = $rt; # Get original author and content - my $original_author = $rt->{'user'}->{'acct'} || $rt->{'user'}->{'screen_name'} || 'unknown_user'; + my $original_author = $rt->{'user'}->{'acct'} || $rt->{'user'}->{'username'} || 'unknown_user'; my $content = $rt->{'text'} || ''; # Get booster (who shared this) - my $booster = $i->{'user'}->{'acct'} || $i->{'user'}->{'screen_name'} || 'unknown_booster'; + my $booster = $i->{'user'}->{'acct'} || $i->{'user'}->{'username'} || 'unknown_booster'; print $stdout "-- DEBUG: Boost - original: '$original_author', booster: '$booster', content: '$content'\n" if ($verbose); # Store boost data to apply after destroy_all_tco $boost_content = $content; - my $original_acct = $rt->{'user'}->{'acct'} || $rt->{'user'}->{'screen_name'} || $original_author; + my $original_acct = $rt->{'user'}->{'acct'} || $rt->{'user'}->{'username'} || $original_author; $boost_attribution = $original_acct; # Set booster as the main user (who performed the boost action)