Couple of bug fixes for /followers. Added /following.. Fixed the /follow command.
This commit is contained in:
+625
-79
@@ -146,6 +146,9 @@ BEGIN {
|
|||||||
# === DIRECT MESSAGES ===
|
# === DIRECT MESSAGES ===
|
||||||
# dmpause=0 # DM refresh rate (0=use main pause)
|
# dmpause=0 # DM refresh rate (0=use main pause)
|
||||||
|
|
||||||
|
# === NOTIFICATIONS ===
|
||||||
|
# notificationpause=0 # Notification refresh rate (0=use main pause)
|
||||||
|
|
||||||
# === INTERACTION ===
|
# === INTERACTION ===
|
||||||
# mentions=0 # Show mentions in timeline
|
# mentions=0 # Show mentions in timeline
|
||||||
# synch=0 # Synchronous mode (blocks on requests)
|
# synch=0 # Synchronous mode (blocks on requests)
|
||||||
@@ -237,7 +240,7 @@ EOF
|
|||||||
signals_use_posix dostream nostreamreplies streamallreplies
|
signals_use_posix dostream nostreamreplies streamallreplies
|
||||||
nofilter
|
nofilter
|
||||||
); %opts_sync = map { $_ => 1 } qw(
|
); %opts_sync = map { $_ => 1 } qw(
|
||||||
ansi pause dmpause ttytteristas verbose superverbose
|
ansi pause dmpause notificationpause ttytteristas verbose superverbose
|
||||||
url rlurl dmurl newline wrap notimeline lists dmidurl
|
url rlurl dmurl newline wrap notimeline lists dmidurl
|
||||||
queryurl track colourprompt colourme notrack
|
queryurl track colourprompt colourme notrack
|
||||||
colourdm colourreply colourwarn coloursearch colourlist idurl
|
colourdm colourreply colourwarn coloursearch colourlist idurl
|
||||||
@@ -247,6 +250,7 @@ EOF
|
|||||||
); %opts_urls = map {$_ => 1} qw(
|
); %opts_urls = map {$_ => 1} qw(
|
||||||
url dmurl uurl rurl wurl frurl rlurl update shoreblogurl
|
url dmurl uurl rurl wurl frurl rlurl update shoreblogurl
|
||||||
apibase fediverseserver queryurl idurl delurl dmdelurl favsurl
|
apibase fediverseserver queryurl idurl delurl dmdelurl favsurl
|
||||||
|
notificationurl markersurl
|
||||||
favurl favdelurl followurl leaveurl
|
favurl favdelurl followurl leaveurl
|
||||||
dmupdate credurl blockurl blockdelurl friendsurl
|
dmupdate credurl blockurl blockdelurl friendsurl
|
||||||
modifyliurl adduliurl delliurl getliurl getlisurl getfliurl
|
modifyliurl adduliurl delliurl getliurl getlisurl getfliurl
|
||||||
@@ -265,7 +269,7 @@ EOF
|
|||||||
);
|
);
|
||||||
|
|
||||||
%opts_can_set = map { $_ => 1 } qw(
|
%opts_can_set = map { $_ => 1 } qw(
|
||||||
url pause dmurl dmpause dmmarkread superverbose ansi verbose
|
url pause dmurl dmpause notificationpause dmmarkread superverbose ansi verbose
|
||||||
update uurl rurl wurl avatar ttytteristas frurl track
|
update uurl rurl wurl avatar ttytteristas frurl track
|
||||||
rlurl noprompt shoreblogurl newline wrap verify autosplit
|
rlurl noprompt shoreblogurl newline wrap verify autosplit
|
||||||
notimeline queryurl fediverseserver colourprompt colourme
|
notimeline queryurl fediverseserver colourprompt colourme
|
||||||
@@ -672,15 +676,16 @@ if ($termrl && $termrl->ReadLine eq 'Term::ReadLine::Gnu') {
|
|||||||
/help /? /quit /q /bye /end /e /exit
|
/help /? /quit /q /bye /end /e /exit
|
||||||
/refresh /r /thump /again /a
|
/refresh /r /thump /again /a
|
||||||
/dm /dmr /dmrefresh /dms /dmsent /dmagain
|
/dm /dmr /dmrefresh /dms /dmsent /dmagain
|
||||||
|
/notifications /notificationrefresh /nr
|
||||||
/replies /re /reply /timeline /timelines
|
/replies /re /reply /timeline /timelines
|
||||||
/media /visibility /search /se
|
/media /poll /mpoll /visibility /search /se
|
||||||
/history /h /print /p /verbose /ve
|
/history /h /print /p /verbose /ve
|
||||||
/ruler /ru /cls /clear /url /open
|
/ruler /ru /cls /clear /url /open
|
||||||
/short /sh /rate /ratelimit
|
/short /sh /rate /ratelimit
|
||||||
/track /tron /troff /trends /woeids
|
/track /tron /troff /trends /woeids
|
||||||
/notrack /set /unset /add /del
|
/notrack /set /unset /add /del
|
||||||
/push /pop /list /lists /listfollowers
|
/push /pop /list /lists /listfollowers
|
||||||
/listfriends /dump /du /eval /ev
|
/listfriends /followers /following /dump /du /eval /ev
|
||||||
/version /update /versioncheck /updatecheck
|
/version /update /versioncheck /updatecheck
|
||||||
/thread /th /entities /ent /delete
|
/thread /th /entities /ent /delete
|
||||||
/deletelast /rtsof /vote /whois /w /me
|
/deletelast /rtsof /vote /whois /w /me
|
||||||
@@ -1344,6 +1349,9 @@ $dmupdate ||= "${apibase}/statuses"; # DMs are private statuses
|
|||||||
$dmdelurl ||= "${apibase}/statuses/%I";
|
$dmdelurl ||= "${apibase}/statuses/%I";
|
||||||
$dmidurl ||= "${apibase}/statuses/%I";
|
$dmidurl ||= "${apibase}/statuses/%I";
|
||||||
|
|
||||||
|
$notificationurl ||= "${apibase}/notifications";
|
||||||
|
$markersurl ||= "${apibase}/markers";
|
||||||
|
|
||||||
$favsurl ||= "${apibase}/favourites";
|
$favsurl ||= "${apibase}/favourites";
|
||||||
$favurl ||= "${apibase}/statuses/%I/favourite";
|
$favurl ||= "${apibase}/statuses/%I/favourite";
|
||||||
$favdelurl ||= "${apibase}/statuses/%I/unfavourite";
|
$favdelurl ||= "${apibase}/statuses/%I/unfavourite";
|
||||||
@@ -1396,6 +1404,9 @@ $dmpause = 4 if (!defined $dmpause); # NOT ||= ... zero is a VALID value!
|
|||||||
$dmpause = 0 if ($anonymous);
|
$dmpause = 0 if ($anonymous);
|
||||||
$dmpause = 0 if ($pause eq '0');
|
$dmpause = 0 if ($pause eq '0');
|
||||||
$dmmarkread = 1 if (!defined $dmmarkread); # Default to enabled
|
$dmmarkread = 1 if (!defined $dmmarkread); # Default to enabled
|
||||||
|
$notificationpause = 6 if (!defined $notificationpause); # NOT ||= ... zero is a VALID value!
|
||||||
|
$notificationpause = 0 if ($anonymous);
|
||||||
|
$notificationpause = 0 if ($pause eq '0');
|
||||||
$ansi = ($noansi) ? 0 :
|
$ansi = ($noansi) ? 0 :
|
||||||
(($ansi || $ENV{'TERM'} eq 'ansi' || $ENV{'TERM'} eq 'xterm-color')
|
(($ansi || $ENV{'TERM'} eq 'ansi' || $ENV{'TERM'} eq 'xterm-color')
|
||||||
? 1 : 0);
|
? 1 : 0);
|
||||||
@@ -1404,9 +1415,11 @@ $ansi = ($noansi) ? 0 :
|
|||||||
if ($synch) {
|
if ($synch) {
|
||||||
$pause = 0;
|
$pause = 0;
|
||||||
$dmpause = ($dmpause) ? 1 : 0;
|
$dmpause = ($dmpause) ? 1 : 0;
|
||||||
|
$notificationpause = ($notificationpause) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dmcount = $dmpause;
|
$dmcount = $dmpause;
|
||||||
|
$notificationcount = $notificationpause;
|
||||||
$lastshort = undef;
|
$lastshort = undef;
|
||||||
|
|
||||||
# ANSI sequences
|
# ANSI sequences
|
||||||
@@ -2008,7 +2021,7 @@ exit(0) if (length($status));
|
|||||||
if (length($credentials)) {
|
if (length($credentials)) {
|
||||||
print "-- processing credentials: ";
|
print "-- processing credentials: ";
|
||||||
$my_json_ref = &map_mastodon_fields(&parsejson($credentials));
|
$my_json_ref = &map_mastodon_fields(&parsejson($credentials));
|
||||||
$whoami = lc($my_json_ref->{'username'} || $my_json_ref->{'acct'});
|
$whoami = lc($my_json_ref->{'acct'} || $my_json_ref->{'username'});
|
||||||
if (!length($whoami)) {
|
if (!length($whoami)) {
|
||||||
print "FAILED!\nis your account suspended, or wrong token?\n";
|
print "FAILED!\nis your account suspended, or wrong token?\n";
|
||||||
exit;
|
exit;
|
||||||
@@ -2060,6 +2073,7 @@ if ($daemon) {
|
|||||||
}
|
}
|
||||||
$parent = 0;
|
$parent = 0;
|
||||||
$dmcount = 1 if ($dmpause); # force fetch
|
$dmcount = 1 if ($dmpause); # force fetch
|
||||||
|
$notificationcount = 1 if ($notificationpause); # force fetch
|
||||||
$is_background = 1;
|
$is_background = 1;
|
||||||
DAEMONLOOP: for(;;) {
|
DAEMONLOOP: for(;;) {
|
||||||
my $snooze;
|
my $snooze;
|
||||||
@@ -2070,6 +2084,13 @@ if ($daemon) {
|
|||||||
&update_effpause;
|
&update_effpause;
|
||||||
&refresh(0);
|
&refresh(0);
|
||||||
$dont_refresh_first_time = 0;
|
$dont_refresh_first_time = 0;
|
||||||
|
# Check notifications before DMs since DM refresh can return early
|
||||||
|
if ($notificationpause) {
|
||||||
|
if (!--$notificationcount) {
|
||||||
|
¬ificationrefresh(0);
|
||||||
|
$notificationcount = $notificationpause;
|
||||||
|
}
|
||||||
|
}
|
||||||
# Move DM refresh after timeline refresh so timeline updates show even if no unread DMs
|
# Move DM refresh after timeline refresh so timeline updates show even if no unread DMs
|
||||||
if ($dmpause) {
|
if ($dmpause) {
|
||||||
if (!--$dmcount) {
|
if (!--$dmcount) {
|
||||||
@@ -3124,6 +3145,12 @@ Just type to talk!
|
|||||||
/media /path/to/file
|
/media /path/to/file
|
||||||
Upload media (images, video, audio) with accessibility features
|
Upload media (images, video, audio) with accessibility features
|
||||||
|
|
||||||
|
/poll Your question here
|
||||||
|
Create a single choice poll
|
||||||
|
|
||||||
|
/mpoll Your question here
|
||||||
|
Create a multiple choice poll (users can select multiple options)
|
||||||
|
|
||||||
/quit
|
/quit
|
||||||
Resumes your boring life.
|
Resumes your boring life.
|
||||||
|
|
||||||
@@ -3144,6 +3171,8 @@ USER COMMANDS:
|
|||||||
/wagain username - combines them all
|
/wagain username - combines them all
|
||||||
/follow username - follow a username
|
/follow username - follow a username
|
||||||
/leave username - stop following a username
|
/leave username - stop following a username
|
||||||
|
/followers [username] - show who follows you (or username)
|
||||||
|
/following [username] - show who you follow (or username follows)
|
||||||
/dm username message - send a username a DM
|
/dm username message - send a username a DM
|
||||||
|
|
||||||
POST AND DM SELECTION:
|
POST AND DM SELECTION:
|
||||||
@@ -3239,6 +3268,7 @@ EOF
|
|||||||
if ($dostream);
|
if ($dostream);
|
||||||
&thump;
|
&thump;
|
||||||
&dmthump_no_skip if ($dmpause); # Also refresh DMs but don't skip timeline
|
&dmthump_no_skip if ($dmpause); # Also refresh DMs but don't skip timeline
|
||||||
|
¬ificationthump if ($notificationpause); # Also refresh notifications
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3247,6 +3277,24 @@ EOF
|
|||||||
my $file_path = $1;
|
my $file_path = $1;
|
||||||
return &handle_media_upload($file_path);
|
return &handle_media_upload($file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Poll creation commands
|
||||||
|
if (m#^/poll\s+(.+)$#) {
|
||||||
|
my $poll_text = $1;
|
||||||
|
return &handle_poll_creation($poll_text, 0); # Single choice
|
||||||
|
}
|
||||||
|
if (m#^/poll\s*$#) {
|
||||||
|
print $stdout "-- ERROR: Poll requires question text. Usage: /poll Your question here\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (m#^/mpoll\s+(.+)$#) {
|
||||||
|
my $poll_text = $1;
|
||||||
|
return &handle_poll_creation($poll_text, 1); # Multiple choice
|
||||||
|
}
|
||||||
|
if (m#^/mpoll\s*$#) {
|
||||||
|
print $stdout "-- ERROR: Multiple choice poll requires question text. Usage: /mpoll Your question here\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (m#^/a(gain)?(\s+\+\d+)?$#) { # the asynchronous form
|
if (m#^/a(gain)?(\s+\+\d+)?$#) { # the asynchronous form
|
||||||
my $countmaybe = $2;
|
my $countmaybe = $2;
|
||||||
$countmaybe =~ s/[^\d]//g if (length($countmaybe));
|
$countmaybe =~ s/[^\d]//g if (length($countmaybe));
|
||||||
@@ -3406,7 +3454,7 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
# this is dual-headed and supports both lists and regular followers.
|
# this is dual-headed and supports both lists and regular followers.
|
||||||
if(s#^/(frs|friends|fos|followers)(\s+\+\d+)?\s*##) {
|
if(s#^/(frs|friends|following|fos|followers)(\s+\+\d+)?\s*##) {
|
||||||
my $countmaybe = $2;
|
my $countmaybe = $2;
|
||||||
my $mode = $1;
|
my $mode = $1;
|
||||||
my $arg = lc($_);
|
my $arg = lc($_);
|
||||||
@@ -3423,9 +3471,9 @@ EOF
|
|||||||
}
|
}
|
||||||
$who ||= $whoami;
|
$who ||= $whoami;
|
||||||
if (!length($lname)) {
|
if (!length($lname)) {
|
||||||
$what = ($mode eq 'frs' || $mode eq 'friends')
|
$what = ($mode eq 'frs' || $mode eq 'friends' || $mode eq 'following')
|
||||||
? "friends" : "followers";
|
? "following" : "followers";
|
||||||
$mode = ($mode eq 'frs' || $mode eq 'friends')
|
$mode = ($mode eq 'frs' || $mode eq 'friends' || $mode eq 'following')
|
||||||
? $friendsurl : $followersurl;
|
? $friendsurl : $followersurl;
|
||||||
} else {
|
} else {
|
||||||
# List members/followers - fediverse only supports list members, not subscribers
|
# List members/followers - fediverse only supports list members, not subscribers
|
||||||
@@ -3480,59 +3528,31 @@ EOF
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Use proper Mastodon API endpoint with account ID
|
# Use proper Mastodon API endpoint with account ID - much simpler than Twitter!
|
||||||
my $followers_url = $mode;
|
my $followers_url = $mode;
|
||||||
$followers_url =~ s/%I/$account_id/g;
|
$followers_url =~ s/%I/$account_id/g;
|
||||||
my $accounts_ref = &grabjson("$followers_url?limit=${countmaybe}", 0, 0, 0, undef, 1);
|
|
||||||
|
# Fediverse API directly returns account objects, no need for separate lookup
|
||||||
|
my $limit = &min($countmaybe, 80); # Mastodon default max is 80
|
||||||
|
my $accounts_ref = &grabjson("$followers_url?limit=$limit", 0, 0, 0, undef, 1);
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: API response type: " . (ref($accounts_ref) || 'not a reference') . "\n" if ($verbose);
|
||||||
|
if (ref($accounts_ref) eq 'ARRAY') {
|
||||||
|
print $stdout "-- DEBUG: Number of accounts in response: " . scalar(@{ $accounts_ref }) . "\n" if ($verbose);
|
||||||
|
}
|
||||||
|
|
||||||
return 0 if (!$accounts_ref || ref($accounts_ref) ne 'ARRAY');
|
return 0 if (!$accounts_ref || ref($accounts_ref) ne 'ARRAY');
|
||||||
|
|
||||||
# Fediverse (Mastodon/Pleroma/etc) returns array of account objects, extract IDs
|
print $stdout "-- $what for $who:\n";
|
||||||
my @ids = map { $_->{'id'} } @{ $accounts_ref };
|
|
||||||
@ids = sort { 0+$a <=> 0+$b } @ids;
|
|
||||||
# make it somewhat deterministic
|
|
||||||
|
|
||||||
my $dount = &min($countmaybe, scalar(@ids));
|
|
||||||
my $swallow = &min(100, $dount);
|
|
||||||
my @usarray = undef; shift(@usarray); # force underflow
|
|
||||||
my $l_ref = undef;
|
|
||||||
|
|
||||||
# for each block of $countper, emit
|
|
||||||
my $printed = 0;
|
my $printed = 0;
|
||||||
|
for my $account (@{ $accounts_ref }) {
|
||||||
FFABIO: while ($dount--) {
|
# Fediverse returns complete account objects - just display them
|
||||||
if (!scalar(@usarray)) {
|
print $stdout "-- DEBUG: Processing account: " . ($account->{'username'} || 'no-username') . " / " . ($account->{'acct'} || 'no-acct') . "\n" if ($verbose);
|
||||||
my @next_ids;
|
&$userhandle($account);
|
||||||
|
|
||||||
last FFABIO if (!scalar(@ids));
|
|
||||||
|
|
||||||
# if we asked for less than 100, get
|
|
||||||
# that. otherwise,
|
|
||||||
# get the top 100 off that list (or
|
|
||||||
# the list itself, if 100 or less)
|
|
||||||
if (scalar(@ids) <= $swallow) {
|
|
||||||
@next_ids = @ids;
|
|
||||||
@ids = ();
|
|
||||||
} else {
|
|
||||||
@next_ids =
|
|
||||||
@ids[0..($swallow-1)];
|
|
||||||
@ids = @ids[$swallow..$#ids];
|
|
||||||
}
|
|
||||||
|
|
||||||
# turn it into a list to pass to
|
|
||||||
# lookupidurl and get the list
|
|
||||||
$l_ref = &postjson($lookupidurl,
|
|
||||||
"user_id=".&url_oauth_sub(join(',', @next_ids)));
|
|
||||||
last FFABIO if(ref($l_ref) ne 'ARRAY');
|
|
||||||
@usarray = sort
|
|
||||||
{ 0+($a->{'id'}) <=> 0+($b->{'id'}) }
|
|
||||||
@{ $l_ref };
|
|
||||||
last if (!scalar(@usarray));
|
|
||||||
}
|
|
||||||
&$userhandle(shift(@usarray));
|
|
||||||
$printed++;
|
$printed++;
|
||||||
}
|
}
|
||||||
print $stdout "-- sorry, no $what found for $who.\n"
|
|
||||||
if (!$printed);
|
print $stdout "-- sorry, no $what found for $who.\n" if (!$printed);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4065,14 +4085,33 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) {
|
|||||||
}
|
}
|
||||||
# Use Mastodon's acct field (includes @domain for remote users) or username for local users
|
# Use Mastodon's acct field (includes @domain for remote users) or username for local users
|
||||||
my $target;
|
my $target;
|
||||||
my $acct = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'username'});
|
my $acct = &descape($post->{'account'}->{'acct'} || $post->{'user'}->{'acct'} || $post->{'user'}->{'username'});
|
||||||
$target = $acct;
|
|
||||||
|
|
||||||
# If acct doesn't include @domain and this is a remote post, construct it
|
# Always ensure we have the full @domain format for federation
|
||||||
if ($acct !~ /\@/ && $post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) {
|
if ($acct =~ /\@/) {
|
||||||
my $domain = $1;
|
# Already has domain, use as-is
|
||||||
if ($domain ne $fediverseserver) {
|
$target = $acct;
|
||||||
|
} else {
|
||||||
|
# No domain - extract from post URL, account URI, or other sources
|
||||||
|
my $domain;
|
||||||
|
|
||||||
|
# Try multiple sources for domain information
|
||||||
|
if ($post->{'account'}->{'url'} && $post->{'account'}->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post->{'account'}->{'uri'} && $post->{'account'}->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post->{'uri'} && $post->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($domain) {
|
||||||
$target = "$acct\@$domain";
|
$target = "$acct\@$domain";
|
||||||
|
} else {
|
||||||
|
# Last resort fallback - but this should not happen for federation
|
||||||
|
print $stdout "-- WARNING: Could not determine domain for user $acct, using local server\n" if ($verbose);
|
||||||
|
$target = "$acct\@$fediverseserver";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print $stdout "-- DEBUG: Reply target acct='$post->{'user'}->{'acct'}', username='$post->{'user'}->{'username'}', 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);
|
||||||
@@ -4105,7 +4144,36 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
# in the future, add DM in_reply_to here
|
# in the future, add DM in_reply_to here
|
||||||
my $target = &descape($dm->{'last_status'}->{'account'}->{'acct'} || $dm->{'last_status'}->{'account'}->{'username'});
|
my $acct = &descape($dm->{'last_status'}->{'account'}->{'acct'} || $dm->{'last_status'}->{'account'}->{'username'});
|
||||||
|
my $target;
|
||||||
|
|
||||||
|
# Always ensure we have the full @domain format for federation
|
||||||
|
if ($acct =~ /\@/) {
|
||||||
|
# Already has domain, use as-is
|
||||||
|
$target = $acct;
|
||||||
|
} else {
|
||||||
|
# No domain - extract from DM account URL, URI, or other sources
|
||||||
|
my $domain;
|
||||||
|
|
||||||
|
# Try multiple sources for domain information
|
||||||
|
if ($dm->{'last_status'}->{'account'}->{'url'} && $dm->{'last_status'}->{'account'}->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($dm->{'last_status'}->{'url'} && $dm->{'last_status'}->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($dm->{'last_status'}->{'account'}->{'uri'} && $dm->{'last_status'}->{'account'}->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($dm->{'last_status'}->{'uri'} && $dm->{'last_status'}->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($domain) {
|
||||||
|
$target = "$acct\@$domain";
|
||||||
|
} else {
|
||||||
|
# Last resort fallback - but this should not happen for federation
|
||||||
|
print $stdout "-- WARNING: Could not determine domain for DM user $acct, using local server\n" if ($verbose);
|
||||||
|
$target = "$acct\@$fediverseserver";
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($termrl) {
|
if ($termrl) {
|
||||||
$readline_completion{'@'.lc($target)}++;
|
$readline_completion{'@'.lc($target)}++;
|
||||||
&save_completion_cache;
|
&save_completion_cache;
|
||||||
@@ -4128,14 +4196,33 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) {
|
|||||||
}
|
}
|
||||||
# Use Mastodon's acct field (includes @domain for remote users) or username for local users
|
# Use Mastodon's acct field (includes @domain for remote users) or username for local users
|
||||||
my $target;
|
my $target;
|
||||||
my $acct = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'username'});
|
my $acct = &descape($post->{'account'}->{'acct'} || $post->{'user'}->{'acct'} || $post->{'user'}->{'username'});
|
||||||
$target = $acct;
|
|
||||||
|
|
||||||
# If acct doesn't include @domain and this is a remote post, construct it
|
# Always ensure we have the full @domain format for federation
|
||||||
if ($acct !~ /\@/ && $post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) {
|
if ($acct =~ /\@/) {
|
||||||
my $domain = $1;
|
# Already has domain, use as-is
|
||||||
if ($domain ne $fediverseserver) {
|
$target = $acct;
|
||||||
|
} else {
|
||||||
|
# No domain - extract from post URL, account URI, or other sources
|
||||||
|
my $domain;
|
||||||
|
|
||||||
|
# Try multiple sources for domain information
|
||||||
|
if ($post->{'account'}->{'url'} && $post->{'account'}->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post->{'url'} && $post->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post->{'account'}->{'uri'} && $post->{'account'}->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post->{'uri'} && $post->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($domain) {
|
||||||
$target = "$acct\@$domain";
|
$target = "$acct\@$domain";
|
||||||
|
} else {
|
||||||
|
# Last resort fallback - but this should not happen for federation
|
||||||
|
print $stdout "-- WARNING: Could not determine domain for user $acct, using local server\n" if ($verbose);
|
||||||
|
$target = "$acct\@$fediverseserver";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
my $text = $_;
|
my $text = $_;
|
||||||
@@ -4284,6 +4371,12 @@ EOF
|
|||||||
&dmthump;
|
&dmthump;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
if ($_ eq '/notifications' || $_ eq '/notificationrefresh' || $_ eq '/nr') {
|
||||||
|
¬ificationthump;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
# /dmsent, /dmagain
|
# /dmsent, /dmagain
|
||||||
if (m#^/dm(s|sent|a|again)(\s+\+\d+)?$#) {
|
if (m#^/dm(s|sent|a|again)(\s+\+\d+)?$#) {
|
||||||
my $mode = $1;
|
my $mode = $1;
|
||||||
@@ -4838,7 +4931,36 @@ sub reply_to_all {
|
|||||||
|
|
||||||
# Get the post content and author
|
# Get the post content and author
|
||||||
my $post_content = $post_ref->{'text'} || '';
|
my $post_content = $post_ref->{'text'} || '';
|
||||||
my $post_author = $post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'username'} || '';
|
my $acct = $post_ref->{'account'}->{'acct'} || $post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'username'} || '';
|
||||||
|
my $post_author;
|
||||||
|
|
||||||
|
# Always ensure we have the full @domain format for federation
|
||||||
|
if ($acct =~ /\@/) {
|
||||||
|
# Already has domain, use as-is
|
||||||
|
$post_author = $acct;
|
||||||
|
} else {
|
||||||
|
# No domain - extract from post URL, account URI, or other sources
|
||||||
|
my $domain;
|
||||||
|
|
||||||
|
# Try multiple sources for domain information
|
||||||
|
if ($post_ref->{'account'}->{'url'} && $post_ref->{'account'}->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post_ref->{'url'} && $post_ref->{'url'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post_ref->{'account'}->{'uri'} && $post_ref->{'account'}->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
} elsif ($post_ref->{'uri'} && $post_ref->{'uri'} =~ m{^https?://([^/]+)/}) {
|
||||||
|
$domain = $1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($domain) {
|
||||||
|
$post_author = "$acct\@$domain";
|
||||||
|
} else {
|
||||||
|
# Last resort fallback - but this should not happen for federation
|
||||||
|
print $stdout "-- WARNING: Could not determine domain for user $acct in reply-to-all, using local server\n" if ($verbose);
|
||||||
|
$post_author = "$acct\@$fediverseserver";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Set up reply-to ID
|
# Set up reply-to ID
|
||||||
my $in_reply_to = $post_ref->{'id_str'} || $post_ref->{'id'};
|
my $in_reply_to = $post_ref->{'id_str'} || $post_ref->{'id'};
|
||||||
@@ -4953,6 +5075,7 @@ sub sub_helper {
|
|||||||
sub sync_console {
|
sub sync_console {
|
||||||
&thump;
|
&thump;
|
||||||
&dmthump unless (!$dmpause);
|
&dmthump unless (!$dmpause);
|
||||||
|
¬ificationthump unless (!$notificationpause);
|
||||||
}
|
}
|
||||||
sub sync_semaphore {
|
sub sync_semaphore {
|
||||||
if ($synch) {
|
if ($synch) {
|
||||||
@@ -5044,6 +5167,12 @@ $stream_failure = 0;
|
|||||||
$dm_first_time = ($dmpause) ? 1 : 0;
|
$dm_first_time = ($dmpause) ? 1 : 0;
|
||||||
$dm_display_only = 0; # Flag to suppress notifications during user-initiated /dms commands
|
$dm_display_only = 0; # Flag to suppress notifications during user-initiated /dms commands
|
||||||
$dm_notification_sent = 0; # Flag to prevent duplicate notifications per refresh cycle
|
$dm_notification_sent = 0; # Flag to prevent duplicate notifications per refresh cycle
|
||||||
|
$notification_first_time = ($notificationpause) ? 1 : 0;
|
||||||
|
$notification_display_only = 0; # Flag to suppress notifications during user-initiated notification commands
|
||||||
|
$notification_notification_sent = 0; # Flag to prevent duplicate notifications per refresh cycle
|
||||||
|
$last_notification_marker = ''; # Track last read notification ID via markers API
|
||||||
|
# Notification tracking uses markers API for server-side read status
|
||||||
|
%notification_seen = (); # Hash to track seen notification IDs locally
|
||||||
# DM tracking now uses Mastodon's unread flag with local read tracking fallback
|
# DM tracking now uses Mastodon's unread flag with local read tracking fallback
|
||||||
%dm_seen_status = (); # Hash to track seen conversation_id:last_status_id pairs
|
%dm_seen_status = (); # Hash to track seen conversation_id:last_status_id pairs
|
||||||
&load_dm_seen_status(); # Load persistent tracking data
|
&load_dm_seen_status(); # Load persistent tracking data
|
||||||
@@ -5062,6 +5191,21 @@ for(;;) {
|
|||||||
(!$effpause && !$interactive);
|
(!$effpause && !$interactive);
|
||||||
$dont_refresh_first_time = 0;
|
$dont_refresh_first_time = 0;
|
||||||
$previous_last_id = $last_id;
|
$previous_last_id = $last_id;
|
||||||
|
if ($notificationpause && ($effpause || $synch)) {
|
||||||
|
if ($notification_first_time) {
|
||||||
|
¬ificationrefresh(0);
|
||||||
|
$notificationcount = $notificationpause;
|
||||||
|
} elsif (!$interactive) {
|
||||||
|
print $stdout "-- DEBUG: Notification countdown: $notificationcount -> " . ($notificationcount - 1) . "\n" if ($verbose);
|
||||||
|
if (!--$notificationcount) {
|
||||||
|
print $stdout "-- DEBUG: Triggering background notification refresh\n" if ($verbose);
|
||||||
|
¬ificationrefresh($interactive);
|
||||||
|
$notificationcount = $notificationpause;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print $stdout "-- DEBUG: Skipping notification countdown (interactive=$interactive)\n" if ($verbose);
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($dmpause && ($effpause || $synch)) {
|
if ($dmpause && ($effpause || $synch)) {
|
||||||
if ($dm_first_time) {
|
if ($dm_first_time) {
|
||||||
&dmrefresh(0);
|
&dmrefresh(0);
|
||||||
@@ -5259,7 +5403,7 @@ EOF
|
|||||||
$key->{'tag'}->{'type'}. " ". # NO SPACES!
|
$key->{'tag'}->{'type'}. " ". # NO SPACES!
|
||||||
unpack("${pack_magic}H*", $key->{'tag'}->{'payload'}). " ".
|
unpack("${pack_magic}H*", $key->{'tag'}->{'payload'}). " ".
|
||||||
($key->{'reblogs_count'} || "0") . " " .
|
($key->{'reblogs_count'} || "0") . " " .
|
||||||
($key->{'user'}->{'username'} || $key->{'user'}->{'acct'})." $ds $src|".
|
($key->{'user'}->{'username'} || $key->{'user'}->{'acct'})." ".($key->{'user'}->{'acct'} || $key->{'user'}->{'username'})." $ds $src|".
|
||||||
unpack("${pack_magic}H*", $key->{'text'}).
|
unpack("${pack_magic}H*", $key->{'text'}).
|
||||||
$space_pad), 0, 1024);
|
$space_pad), 0, 1024);
|
||||||
print P $key;
|
print P $key;
|
||||||
@@ -5414,6 +5558,11 @@ EOF
|
|||||||
&send_repaint if ($termrl);
|
&send_repaint if ($termrl);
|
||||||
$dmcount = $dmpause;
|
$dmcount = $dmpause;
|
||||||
goto DONT_REFRESH;
|
goto DONT_REFRESH;
|
||||||
|
} elsif ($rout =~ /^notificationthump/) {
|
||||||
|
¬ificationrefresh($interactive);
|
||||||
|
&send_repaint if ($termrl);
|
||||||
|
$notificationcount = $notificationpause;
|
||||||
|
goto DONT_REFRESH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -6661,7 +6810,23 @@ sub updatest {
|
|||||||
} else {
|
} else {
|
||||||
if (length($user_name_dm)) {
|
if (length($user_name_dm)) {
|
||||||
# For DMs: include mention in status and set visibility
|
# For DMs: include mention in status and set visibility
|
||||||
my $dm_status = "\@${user_name_dm} ${string}";
|
# Ensure we have the full @domain format for federation
|
||||||
|
my $dm_target = $user_name_dm;
|
||||||
|
if ($dm_target !~ /\@/) {
|
||||||
|
# No domain - try to find full format in completion cache
|
||||||
|
my $found_full = undef;
|
||||||
|
foreach my $cached_user (keys %readline_completion) {
|
||||||
|
if ($cached_user =~ /^\@(.+)\@(.+)$/) {
|
||||||
|
my ($cached_username) = ($1);
|
||||||
|
if (lc($cached_username) eq lc($dm_target)) {
|
||||||
|
$found_full = substr($cached_user, 1); # Remove @ prefix
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$dm_target = $found_full || $dm_target;
|
||||||
|
}
|
||||||
|
my $dm_status = "\@${dm_target} ${string}";
|
||||||
my $dm_urle = '';
|
my $dm_urle = '';
|
||||||
foreach my $char (unpack("${pack_magic}C*", $dm_status)) {
|
foreach my $char (unpack("${pack_magic}C*", $dm_status)) {
|
||||||
my $k = chr($char);
|
my $k = chr($char);
|
||||||
@@ -6746,6 +6911,171 @@ EOF
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# refresh for notifications
|
||||||
|
sub notificationrefresh {
|
||||||
|
my $interactive = shift;
|
||||||
|
|
||||||
|
# Reset notification flag for this refresh cycle
|
||||||
|
$notification_notification_sent = 0;
|
||||||
|
|
||||||
|
if ($anonymous) {
|
||||||
|
print $stdout
|
||||||
|
"-- sorry, you can't read notifications if you're anonymous.\n"
|
||||||
|
if ($interactive);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
# no point in doing this if we can't even get to our own timeline
|
||||||
|
# (unless user specifically requested it, or our timeline is off)
|
||||||
|
return if (!$interactive && !$last_id && !$notimeline);
|
||||||
|
|
||||||
|
# Get current markers to see what's been read
|
||||||
|
my $markers_ref = &grabjson($markersurl . "?timeline[]=notifications",
|
||||||
|
0, 0, 0, undef, 1);
|
||||||
|
|
||||||
|
my $since_id = 0;
|
||||||
|
if (defined($markers_ref) && ref($markers_ref) eq 'HASH' &&
|
||||||
|
$markers_ref->{'notifications'} &&
|
||||||
|
$markers_ref->{'notifications'}->{'last_read_id'}) {
|
||||||
|
$since_id = $markers_ref->{'notifications'}->{'last_read_id'};
|
||||||
|
$last_notification_marker = $since_id;
|
||||||
|
print $stdout "-- DEBUG: Got notification marker: $since_id\n" if ($verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch notifications since the marker
|
||||||
|
my $my_json_ref = &grabjson($notificationurl, $since_id, 0,
|
||||||
|
($interactive ? 20 : 50), undef, 1);
|
||||||
|
|
||||||
|
return if (!defined($my_json_ref) || ref($my_json_ref) ne 'ARRAY');
|
||||||
|
|
||||||
|
my $printed = 0;
|
||||||
|
my $max_id = 0;
|
||||||
|
my $disp_max = &min($print_max, scalar(@{ $my_json_ref }));
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Notification response: " . scalar(@{ $my_json_ref }) . " notifications, disp_max=$disp_max\n" if ($verbose);
|
||||||
|
|
||||||
|
# For background refresh, check if there are any new notifications first
|
||||||
|
if (!$interactive && $disp_max) {
|
||||||
|
my $has_new = 0;
|
||||||
|
for (my $check_i = 0; $check_i < $disp_max; $check_i++) {
|
||||||
|
my $check_notif = $my_json_ref->[$check_i];
|
||||||
|
next if (!$check_notif->{'id'});
|
||||||
|
my $notif_id = $check_notif->{'id'};
|
||||||
|
|
||||||
|
# Skip if we've already seen this notification locally
|
||||||
|
if ($notification_seen{$notif_id}) {
|
||||||
|
print $stdout "-- DEBUG: Skipping notification $notif_id - already seen locally\n" if ($verbose);
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
$has_new = 1;
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
if (!$has_new) {
|
||||||
|
print $stdout "-- DEBUG: No new notifications found in background refresh, returning early\n" if ($verbose);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
print $stdout "-- DEBUG: Found new notifications in background refresh, proceeding\n" if ($verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($disp_max) {
|
||||||
|
if ($notification_first_time) {
|
||||||
|
sleep 5 while ($suspend_output > 0);
|
||||||
|
&send_removereadline if ($termrl);
|
||||||
|
print $stdout
|
||||||
|
"-- checking for most recent notifications:\n";
|
||||||
|
$disp_max = 3;
|
||||||
|
$interactive = 1;
|
||||||
|
$notification_display_only = 1; # Suppress sounds during first-time load
|
||||||
|
print $stdout "-- DEBUG: notification_first_time: disp_max reduced to $disp_max, sounds suppressed\n" if ($verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Starting notification display loop: $disp_max notifications\n" if ($verbose);
|
||||||
|
for(my $i = $disp_max; $i > 0; $i--) {
|
||||||
|
my $g = ($i-1);
|
||||||
|
my $notif = $my_json_ref->[$g];
|
||||||
|
print $stdout "-- DEBUG: Processing notification #$i (index $g)\n" if ($verbose);
|
||||||
|
|
||||||
|
# Skip if missing data
|
||||||
|
if (!$notif->{'id'} || !$notif->{'type'}) {
|
||||||
|
print $stdout "-- DEBUG: Skipping notification #$i - missing id or type\n" if ($verbose);
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $notif_id = $notif->{'id'};
|
||||||
|
|
||||||
|
# For background refresh, skip notifications we've already seen locally
|
||||||
|
if (!$interactive) {
|
||||||
|
if ($notification_seen{$notif_id}) {
|
||||||
|
print $stdout "-- DEBUG: Skipping notification #$i - already seen locally (id: $notif_id)\n" if ($verbose);
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mark as seen locally for this session
|
||||||
|
$notification_seen{$notif_id} = 1;
|
||||||
|
print $stdout "-- DEBUG: Marked notification $notif_id as seen locally\n" if ($verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Track highest ID for marker update
|
||||||
|
my $current_id = 0+$notif_id;
|
||||||
|
$max_id = $current_id if ($current_id > $max_id);
|
||||||
|
|
||||||
|
# Process notification with proper type mapping for sounds
|
||||||
|
my $type = $notif->{'type'} || 'default';
|
||||||
|
my $sound_class = $type;
|
||||||
|
$sound_class = 'mention' if ($type eq 'mention');
|
||||||
|
$sound_class = 'boost' if ($type eq 'reblog');
|
||||||
|
$sound_class = 'favourite' if ($type eq 'favourite');
|
||||||
|
$sound_class = 'follow' if ($type eq 'follow');
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Processing notification type '$type' as sound class '$sound_class'\n" if ($verbose);
|
||||||
|
|
||||||
|
# Use existing notification display handler
|
||||||
|
if ($notification_display_only) {
|
||||||
|
# During first-time load, display without sounds
|
||||||
|
print $stdout "-- DEBUG: Displaying notification without sound (display_only mode)\n" if ($verbose);
|
||||||
|
&$handle($notif, ''); # Empty class = no sound
|
||||||
|
} else {
|
||||||
|
¬ifications_tdisplay([ $notif ]);
|
||||||
|
}
|
||||||
|
$printed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep 5 while ($suspend_output > 0);
|
||||||
|
if (($interactive || $verbose) && !$printed && !$notification_first_time) {
|
||||||
|
&send_removereadline if ($termrl);
|
||||||
|
print $stdout "-- no new notifications.\n";
|
||||||
|
$wrapseq = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update marker to highest ID we processed (for background refresh or manual check)
|
||||||
|
if (!$interactive || $notification_first_time || ($interactive && $max_id > 0)) {
|
||||||
|
if ($max_id > 0) {
|
||||||
|
my $marker_data = "notifications%5Blast_read_id%5D=$max_id";
|
||||||
|
my $marker_result = &backticks($baseagent, '/dev/null', undef,
|
||||||
|
$markersurl, $marker_data, 0, @wend);
|
||||||
|
|
||||||
|
if ($? == 0) {
|
||||||
|
$last_notification_marker = $max_id;
|
||||||
|
print $stdout "-- DEBUG: Updated notification marker to $max_id\n" if ($verbose);
|
||||||
|
} else {
|
||||||
|
print $stdout "-- DEBUG: Failed to update notification marker (exit code: $?)\n" if ($verbose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Updated last_notification_marker to $last_notification_marker (interactive=$interactive, notification_first_time=$notification_first_time, max_id=$max_id)\n" if ($verbose);
|
||||||
|
} else {
|
||||||
|
print $stdout "-- DEBUG: NOT updating notification marker (interactive=$interactive, keeping last_notification_marker=$last_notification_marker)\n" if ($verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
$notification_first_time = 0 if ($max_id || !scalar(@{ $my_json_ref }));
|
||||||
|
$notification_display_only = 0; # Reset sound suppression flag
|
||||||
|
print $stdout "-- notification bookmark is $last_notification_marker.\n" if ($verbose);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
# this dispatch routine replaces the common logic of deletest, deletedm,
|
# this dispatch routine replaces the common logic of deletest, deletedm,
|
||||||
# follow, leave and the favourites system.
|
# follow, leave and the favourites system.
|
||||||
# this is a modified, abridged version of &updatest.
|
# this is a modified, abridged version of &updatest.
|
||||||
@@ -6834,10 +7164,48 @@ sub foruuser {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Substitute account ID into URL template
|
||||||
|
my $api_url = $basef;
|
||||||
|
$api_url =~ s/%I/$account_id/g;
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Calling follow/unfollow API with account_id=$account_id, basef=$basef\n" if ($verbose);
|
||||||
my ($en, $em) = ¢ral_cd_dispatch("id=$account_id",
|
my ($en, $em) = ¢ral_cd_dispatch("id=$account_id",
|
||||||
$interactive, $basef);
|
$interactive, $api_url);
|
||||||
print $stdout "-- ok, you have $verb following user $uname.\n"
|
print $stdout "-- DEBUG: Follow API returned - error code: $en, message length: " . length($em) . "\n" if ($verbose);
|
||||||
if ($interactive && !$en);
|
print $stdout "-- DEBUG: Follow API response: $em\n" if ($verbose && $em);
|
||||||
|
|
||||||
|
# Check for HTTP error responses in the message
|
||||||
|
if (!$en && $em && $em =~ /<title>(\d+)\s+([^<]+)<\/title>/) {
|
||||||
|
my $http_code = $1;
|
||||||
|
my $http_message = $2;
|
||||||
|
print $stdout "-- ERROR: $verb follow failed - HTTP $http_code: $http_message\n" if ($interactive);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse JSON response to show accurate relationship status
|
||||||
|
if (!$en && $interactive && $em) {
|
||||||
|
my $response_data = &parsejson($em);
|
||||||
|
if (ref($response_data) eq 'HASH') {
|
||||||
|
if ($verb eq 'started') {
|
||||||
|
if ($response_data->{'following'}) {
|
||||||
|
print $stdout "-- ok, you are now following user $uname.\n";
|
||||||
|
} elsif ($response_data->{'requested'}) {
|
||||||
|
print $stdout "-- ok, follow request sent to user $uname (awaiting approval).\n";
|
||||||
|
} else {
|
||||||
|
print $stdout "-- ok, you have $verb following user $uname.\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# For unfollow
|
||||||
|
if (!$response_data->{'following'} && !$response_data->{'requested'}) {
|
||||||
|
print $stdout "-- ok, you are no longer following user $uname.\n";
|
||||||
|
} else {
|
||||||
|
print $stdout "-- ok, you have $verb following user $uname.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print $stdout "-- ok, you have $verb following user $uname.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6855,8 +7223,12 @@ sub boruuser {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Substitute account ID into URL template
|
||||||
|
my $api_url = $basef;
|
||||||
|
$api_url =~ s/%I/$account_id/g;
|
||||||
|
|
||||||
my ($en, $em) = ¢ral_cd_dispatch("id=$account_id",
|
my ($en, $em) = ¢ral_cd_dispatch("id=$account_id",
|
||||||
$interactive, $basef);
|
$interactive, $api_url);
|
||||||
print $stdout "-- ok, you have $verb blocking user $uname.\n"
|
print $stdout "-- ok, you have $verb blocking user $uname.\n"
|
||||||
if ($interactive && !$en);
|
if ($interactive && !$en);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -7818,17 +8190,42 @@ sub lookup_account_id {
|
|||||||
|
|
||||||
print $stdout "-- DEBUG: Looking up account ID for username: $username\n" if ($verbose);
|
print $stdout "-- DEBUG: Looking up account ID for username: $username\n" if ($verbose);
|
||||||
|
|
||||||
|
# Special case: if looking up our own account, use verify_credentials
|
||||||
|
if (lc($username) eq lc($whoami)) {
|
||||||
|
print $stdout "-- DEBUG: Looking up own account via verify_credentials\n" if ($verbose);
|
||||||
|
my $creds_result = &grabjson($credurl, 0, 0, 0, undef, 1);
|
||||||
|
if ($creds_result && $creds_result->{'id'}) {
|
||||||
|
print $stdout "-- DEBUG: Found own account ID: " . $creds_result->{'id'} . "\n" if ($verbose);
|
||||||
|
return $creds_result->{'id'};
|
||||||
|
}
|
||||||
|
print $stdout "-- DEBUG: Failed to get own account from verify_credentials\n" if ($verbose);
|
||||||
|
}
|
||||||
|
|
||||||
# Use Mastodon search API to find account
|
# Use Mastodon search API to find account
|
||||||
my $search_result = &grabjson("${searchurl}?q=${username}&type=accounts&limit=1", 0, 0, 0, undef, 1);
|
my $search_url = "${searchurl}?q=${username}&type=accounts&limit=1";
|
||||||
|
print $stdout "-- DEBUG: Search URL: $search_url\n" if ($verbose);
|
||||||
|
my $search_result = &grabjson($search_url, 0, 0, 0, undef, 1);
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Search result: " . ($search_result ? "SUCCESS" : "FAILED") . "\n" if ($verbose);
|
||||||
|
if ($search_result && ref($search_result) eq 'HASH') {
|
||||||
|
print $stdout "-- DEBUG: Search result has accounts: " . ($search_result->{'accounts'} ? "YES" : "NO") . "\n" if ($verbose);
|
||||||
|
if ($search_result->{'accounts'}) {
|
||||||
|
print $stdout "-- DEBUG: Number of accounts found: " . scalar(@{$search_result->{'accounts'}}) . "\n" if ($verbose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($search_result && $search_result->{'accounts'} && @{$search_result->{'accounts'}}) {
|
if ($search_result && $search_result->{'accounts'} && @{$search_result->{'accounts'}}) {
|
||||||
my $account = $search_result->{'accounts'}->[0];
|
my $account = $search_result->{'accounts'}->[0];
|
||||||
my $found_username = $account->{'username'} || $account->{'acct'};
|
my $found_username = $account->{'username'} || $account->{'acct'};
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Found account - username: '" . ($account->{'username'} || 'null') . "', acct: '" . ($account->{'acct'} || 'null') . "'\n" if ($verbose);
|
||||||
|
|
||||||
# Verify we found the right account (case-insensitive match)
|
# Verify we found the right account (case-insensitive match)
|
||||||
if (lc($found_username) eq lc($username) || lc($account->{'acct'}) eq lc($username)) {
|
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);
|
print $stdout "-- DEBUG: Found account ID: " . $account->{'id'} . " for $username\n" if ($verbose);
|
||||||
return $account->{'id'};
|
return $account->{'id'};
|
||||||
|
} else {
|
||||||
|
print $stdout "-- DEBUG: Username mismatch - looking for '$username', found '$found_username'\n" if ($verbose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7925,7 +8322,7 @@ sub defaultautocompletion {
|
|||||||
'/orepost', '/erepost', '/frepost', '/liston',
|
'/orepost', '/erepost', '/frepost', '/liston',
|
||||||
'/listoff', '/dmsent', '/rtsof', '/rtson', '/rtsoff',
|
'/listoff', '/dmsent', '/rtsof', '/rtson', '/rtsoff',
|
||||||
'/lists', '/withlist', '/add', '/padd', '/push',
|
'/lists', '/withlist', '/add', '/padd', '/push',
|
||||||
'/pop', '/followers', '/friends', '/lfollow',
|
'/pop', '/followers', '/following', '/friends', '/lfollow',
|
||||||
'/lleave', '/listfollowers', '/listfriends',
|
'/lleave', '/listfollowers', '/listfriends',
|
||||||
'/unset', '/verbose', '/short', '/follow', '/unfollow',
|
'/unset', '/verbose', '/short', '/follow', '/unfollow',
|
||||||
'/doesfollow', '/search', '/tron', '/troff',
|
'/doesfollow', '/search', '/tron', '/troff',
|
||||||
@@ -8105,8 +8502,8 @@ sub get_post {
|
|||||||
$w->{'tag'}->{'type'},
|
$w->{'tag'}->{'type'},
|
||||||
$w->{'tag'}->{'payload'},
|
$w->{'tag'}->{'payload'},
|
||||||
$w->{'reblogs_count'},
|
$w->{'reblogs_count'},
|
||||||
$w->{'user'}->{'username'}, $w->{'created_at'},
|
$w->{'user'}->{'username'}, $w->{'user'}->{'acct'}, $w->{'created_at'},
|
||||||
$l) = split(/\s/, $k, 17);
|
$l) = split(/\s/, $k, 18);
|
||||||
($w->{'source'}, $k) = split(/\|/, $l, 2);
|
($w->{'source'}, $k) = split(/\|/, $l, 2);
|
||||||
$w->{'text'} = pack("H*", $k);
|
$w->{'text'} = pack("H*", $k);
|
||||||
$w->{'place'}->{'full_name'} = pack("H*",$w->{'place'}->{'full_name'});
|
$w->{'place'}->{'full_name'} = pack("H*",$w->{'place'}->{'full_name'});
|
||||||
@@ -8220,6 +8617,7 @@ sub thump {
|
|||||||
}
|
}
|
||||||
sub dmthump { print C "dmthump------------\n"; &sync_semaphore; }
|
sub dmthump { print C "dmthump------------\n"; &sync_semaphore; }
|
||||||
sub dmthump_no_skip { print C "dmthump_no_skip----\n"; &sync_semaphore; }
|
sub dmthump_no_skip { print C "dmthump_no_skip----\n"; &sync_semaphore; }
|
||||||
|
sub notificationthump { print C "notificationthump--\n"; &sync_semaphore; }
|
||||||
|
|
||||||
sub sync_n_quit {
|
sub sync_n_quit {
|
||||||
# Save completion cache before exiting
|
# Save completion cache before exiting
|
||||||
@@ -8663,6 +9061,154 @@ sub create_post_with_media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
##### Poll Creation Functions #####
|
||||||
|
|
||||||
|
sub handle_poll_creation {
|
||||||
|
my $poll_text = shift;
|
||||||
|
my $multiple_choice = shift || 0; # Default to single choice
|
||||||
|
|
||||||
|
my $poll_type = $multiple_choice ? "multiple choice" : "single choice";
|
||||||
|
print $stdout "Creating $poll_type poll: $poll_text\n";
|
||||||
|
|
||||||
|
# Ask for poll duration first (default 24h)
|
||||||
|
print $stdout "Poll duration (default 24h): ";
|
||||||
|
my $duration_input = <STDIN>;
|
||||||
|
chomp($duration_input);
|
||||||
|
|
||||||
|
my $expires_in = &parse_duration($duration_input);
|
||||||
|
|
||||||
|
# Collect poll options interactively
|
||||||
|
my @options = ();
|
||||||
|
my $option_num = 1;
|
||||||
|
|
||||||
|
print $stdout "Enter options (press Enter with no text to finish):\n";
|
||||||
|
|
||||||
|
while ($option_num <= 10) { # Allow up to 10 options (instance may limit to 4)
|
||||||
|
print $stdout "Option $option_num: ";
|
||||||
|
|
||||||
|
# Read user input for option
|
||||||
|
my $option = <STDIN>;
|
||||||
|
chomp($option);
|
||||||
|
|
||||||
|
# If empty input, stop collecting options
|
||||||
|
if ($option eq "") {
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate option length (Mastodon limit is 50 characters)
|
||||||
|
if (length($option) > 50) {
|
||||||
|
print $stdout "-- ERROR: Option too long (max 50 characters): " . length($option) . " chars\n";
|
||||||
|
print $stdout "-- Please enter a shorter option or press Enter to skip.\n";
|
||||||
|
next; # Ask for this option again
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the option to our list
|
||||||
|
push @options, $option;
|
||||||
|
$option_num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate we have at least 2 options
|
||||||
|
if (scalar(@options) < 2) {
|
||||||
|
print $stdout "-- ERROR: Poll cancelled. Need at least 2 options.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create and post the poll
|
||||||
|
return &create_poll_post($poll_text, \@options, $expires_in, $multiple_choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub parse_duration {
|
||||||
|
my $input = shift;
|
||||||
|
|
||||||
|
# Default to 24 hours if empty
|
||||||
|
return 24 * 3600 if ($input eq "");
|
||||||
|
|
||||||
|
# Parse duration format: number + letter (h/d/m for hours/days/minutes)
|
||||||
|
if ($input =~ /^(\d+)([hdm])$/i) {
|
||||||
|
my ($num, $unit) = ($1, lc($2));
|
||||||
|
my $hours;
|
||||||
|
|
||||||
|
if ($unit eq 'h') {
|
||||||
|
$hours = $num;
|
||||||
|
} elsif ($unit eq 'd') {
|
||||||
|
$hours = $num * 24;
|
||||||
|
} elsif ($unit eq 'm') {
|
||||||
|
$hours = $num / 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate bounds
|
||||||
|
if ($hours <= 0) {
|
||||||
|
print $stdout "-- ERROR: Invalid duration. Using default 24 hours.\n";
|
||||||
|
return 24 * 3600;
|
||||||
|
}
|
||||||
|
if ($hours > 8760) { # More than a year
|
||||||
|
print $stdout "-- WARNING: Duration capped at 365 days.\n";
|
||||||
|
$hours = 8760;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int($hours * 3600); # Convert to seconds
|
||||||
|
} else {
|
||||||
|
print $stdout "-- ERROR: Invalid format. Use format like 24h, 3d, 90m. Using default 24h.\n";
|
||||||
|
return 24 * 3600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub create_poll_post {
|
||||||
|
my ($poll_text, $options_ref, $expires_in, $multiple_choice) = @_;
|
||||||
|
my @options = @$options_ref;
|
||||||
|
$multiple_choice = $multiple_choice || 0; # Default to single choice
|
||||||
|
|
||||||
|
# Build the poll parameters for the API
|
||||||
|
my $poll_params = '';
|
||||||
|
for my $i (0 .. $#options) {
|
||||||
|
$poll_params .= "&poll[options][]=" . &url_oauth_sub($options[$i]);
|
||||||
|
}
|
||||||
|
$poll_params .= "&poll[expires_in]=$expires_in";
|
||||||
|
$poll_params .= "&poll[multiple]=" . ($multiple_choice ? "true" : "false");
|
||||||
|
$poll_params .= "&poll[hide_totals]=false";
|
||||||
|
|
||||||
|
# Prepare the status post with poll
|
||||||
|
my $status_param = &url_oauth_sub($poll_text);
|
||||||
|
my $post_data = "status=$status_param$poll_params";
|
||||||
|
|
||||||
|
# Add visibility if set
|
||||||
|
if (defined($visibility)) {
|
||||||
|
$post_data .= "&visibility=$visibility";
|
||||||
|
}
|
||||||
|
|
||||||
|
print $stdout "-- Posting poll with " . scalar(@options) . " options...\n" if ($verbose);
|
||||||
|
|
||||||
|
# Use the same authentication method as regular posts
|
||||||
|
my $post_url = "${apibase}/statuses";
|
||||||
|
print $stdout "-- DEBUG: Poll API call: $post_url\n" if ($verbose);
|
||||||
|
print $stdout "-- DEBUG: Poll data: $post_data\n" if ($superverbose);
|
||||||
|
|
||||||
|
my $return = &backticks($baseagent, '/dev/null', undef, $post_url, $post_data, 0, @wend);
|
||||||
|
|
||||||
|
print $stdout "-- DEBUG: Poll API response length: " . length($return) . " bytes\n" if ($verbose);
|
||||||
|
|
||||||
|
if ($return =~ /"id":\s*"([^"]+)"/) {
|
||||||
|
my $post_id = $1;
|
||||||
|
print $stdout "-- Poll posted successfully (ID: $post_id)\n";
|
||||||
|
print $stdout "-- DEBUG: Poll created with ID: $post_id\n" if ($verbose);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
print $stdout "-- ERROR: Failed to create poll\n";
|
||||||
|
if ($verbose) {
|
||||||
|
print $stdout "-- DEBUG: Full poll response: $return\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Provide helpful error messages for common issues
|
||||||
|
if ($return =~ /too many options/i || $return =~ /exceed.*options/i) {
|
||||||
|
print $stdout "-- This instance may limit polls to 4 options or fewer.\n";
|
||||||
|
} elsif ($return =~ /exceed max chars/i) {
|
||||||
|
print $stdout "-- One or more options exceed the character limit (usually 50).\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
##### optimizers -- these compile into an internal format #####
|
##### optimizers -- these compile into an internal format #####
|
||||||
|
|
||||||
# utility routine for tquery support
|
# utility routine for tquery support
|
||||||
|
|||||||
Reference in New Issue
Block a user