Mostly a bug fix push. Extensions working (for the most part), visibility shownb on the line with the code, that line is pretty much an information line, code, date, client, etc now.

This commit is contained in:
Storm Dragon
2025-07-27 13:46:32 -04:00
parent b92fc9fe47
commit 6bbd2ab263
+414 -66
View File
@@ -48,13 +48,22 @@ BEGIN {
$my_version_string = ($TTYverse_PATCH_VERSION) ? "${TTYverse_VERSION}.${TTYverse_PATCH_VERSION}" : "${TTYverse_VERSION}"; $my_version_string = ($TTYverse_PATCH_VERSION) ? "${TTYverse_VERSION}.${TTYverse_PATCH_VERSION}" : "${TTYverse_VERSION}";
(warn ("$my_version_string\n"), exit) if ($version); (warn ("$my_version_string\n"), exit) if ($version);
# Set up XDG config directory early for -create-rc # Set up XDG directories early for -create-rc and extensions
our $config = ($ENV{'XDG_CONFIG_HOME'} || "$ENV{'HOME'}/.config") . '/ttyverse'; our $config = ($ENV{'XDG_CONFIG_HOME'} || "$ENV{'HOME'}/.config") . '/ttyverse';
# Check if the directory exists; if not, create it our $data = ($ENV{'XDG_DATA_HOME'} || "$ENV{'HOME'}/.local/share") . '/ttyverse';
# Check if directories exist; if not, create them
unless (-d $config) { unless (-d $config) {
eval { require File::Path; File::Path::make_path($config) }; eval { require File::Path; File::Path::make_path($config) };
if ($@) { if ($@) {
die "Failed to create directory: $@"; die "Failed to create config directory: $@";
}
}
unless (-d $data) {
eval { require File::Path; File::Path::make_path("$data/extensions", "$data/sounds/default") };
if ($@) {
die "Failed to create data directory: $@";
} }
} }
@@ -82,24 +91,40 @@ BEGIN {
# fediverseserver=mastodon.social # Your fediverse server # fediverseserver=mastodon.social # Your fediverse server
# ssl=1 # Use SSL/HTTPS (recommended) # ssl=1 # Use SSL/HTTPS (recommended)
# authtype=oauth2 # Authentication type (oauth2 for fediverse) # authtype=oauth2 # Authentication type (oauth2 for fediverse)
# apibase= # Custom API base URL (auto-detected)
# oauthbase= # Custom OAuth base URL (auto-detected)
# === AUTHENTICATION ===
# keyfile=~/.config/ttyverse/key # Path to OAuth key file
# anonymous=0 # Anonymous mode (not supported for fediverse)
# === TIMELINE SETTINGS === # === TIMELINE SETTINGS ===
# pause=auto # Auto-refresh rate (seconds, or 'auto') # pause=auto # Auto-refresh rate (seconds, or 'auto')
# backload=30 # Number of posts to load initially # backload=30 # Number of posts to load initially
# wrap=120 # Text wrapping width # wrap=120 # Text wrapping width
# timestamp=0 # Show timestamps on posts (0=off, 1=relative, 2=both) # timestamp=0 # Show timestamps (0=off, 1=on, format string for custom)
# noreblogs=0 # Hide boost/reblog posts # noreblogs=0 # Hide boost/reblog posts
# notimeline=0 # Disable timeline display
# searchhits=20 # Number of search results to show
# === DISPLAY SETTINGS === # === DISPLAY SETTINGS ===
# ansi=1 # Use ANSI colors # ansi=1 # Use ANSI colors
# noansi=0 # Force disable colors
# verbose=0 # Show debug information # verbose=0 # Show debug information
# superverbose=0 # Even more debug information
# silent=0 # Reduce output messages # silent=0 # Reduce output messages
# readline=1 # Use readline for input (if available) # readline=1 # Use readline for input (if available)
# readlinerepaint=0 # Repaint readline on signals
# vcheck=1 # Check for updates on startup # vcheck=1 # Check for updates on startup
# noprompt=0 # Disable interactive prompts
# newline=0 # Add extra newlines
# === POST SETTINGS === # === POST SETTINGS ===
# post_visibility=public # Default post visibility (public, unlisted, private, direct) # post_visibility=public # Default post visibility (public, unlisted, private, direct)
# linelength=5000 # Maximum post length # linelength=5000 # Maximum post length
# autosplit=0 # Auto-split long posts
# slowpost=0 # Slower posting for rate limiting
# verify=0 # Verify posts before sending
# === COLORS === # === COLORS ===
# colourprompt=CYAN # Prompt color # colourprompt=CYAN # Prompt color
@@ -111,25 +136,75 @@ BEGIN {
# colourlist=OFF # List posts color # colourlist=OFF # List posts color
# colourdefault=OFF # Default text color # colourdefault=OFF # Default text color
# === ADVANCED SETTINGS === # === STREAMING API ===
# dostream=0 # Enable streaming API (real-time updates) # dostream=0 # Enable streaming API (real-time updates)
# synch=0 # Synchronous mode (blocks on requests) # nostreamreplies=0 # Don't stream replies
# maxhist=10 # Command history size # streamallreplies=0 # Stream all replies (not just to you)
# location=0 # Include location in posts # eventbuf=0 # Event buffer size for streaming
# notimeline=0 # Disable timeline display
# daemon=0 # Run as daemon (background)
# === NOTIFICATION SETTINGS === # === DIRECT MESSAGES ===
# dmpause=0 # DM refresh rate (0=use main pause)
# === INTERACTION ===
# mentions=0 # Show mentions in timeline
# synch=0 # Synchronous mode (blocks on requests)
# maxhist=19 # Command history size
# hold=0 # Hold mode for piped input
# daemon=0 # Run as daemon (background)
# script=0 # Script mode (non-interactive)
# === LOCATION ===
# location=0 # Include location in posts
# lat= # Latitude for location
# long= # Longitude for location
# === NOTIFICATIONS ===
# notifies= # Comma-separated list of notification types # notifies= # Comma-separated list of notification types
# notifyquiet=0 # Quiet notifications # notifyquiet=0 # Quiet notifications
# notifytype= # Type of notifications to send
# === FILTER SETTINGS === # === FILTERING ===
# track= # Track keywords (comma-separated) # track= # Track keywords (space-separated)
# filter= # Filter out posts containing these terms # filter= # Filter out posts containing these terms
# notrack=0 # Disable tracking
# filterusers= # Filter posts from specific users
# filterats= # Filter posts with specific @mentions
# filterrts= # Filter retweets/boosts
# filteratonly= # Only show posts with @mentions
# filterflags= # Filter flags
# nofilter=0 # Disable all filtering
# === LISTS ===
# lists= # Comma-separated list of lists to follow
# === URL HANDLING ===
# urlopen=echo %U # Command to open URLs (%U = URL)
# shoreblogurl=http://is.gd/api.php?longurl= # URL shortening service
# === SYSTEM SETTINGS ===
# seven=0 # 7-bit mode for older terminals
# oldperl=0 # Compatibility mode for old Perl
# signals_use_posix=0 # Use POSIX signals (auto-detected)
# nocounter=0 # Disable character counter
# exception_is_maskable=0 # Allow masking exceptions
# simplestart=0 # Simple startup mode
# noratelimit=0 # Ignore rate limiting
# notco=0 # Disable t.co URL expansion
# === ADVANCED/TECHNICAL ===
# runcommand= # Command to run on startup
# twarg= # Legacy Twitter argument (unused)
# user= # Username override
# leader= # Command leader character
# === EXTENSION SETTINGS === # === EXTENSION SETTINGS ===
# Any setting starting with 'extpref_' is for extensions # Extensions can be loaded with the -exts option
# extpref_example_setting=value # Extension preferences use the extpref_ prefix:
# extpref_sound_command=paplay # Sound system command
# extpref_tts_synthesizer=espeak # Text-to-speech engine
# extpref_tts_language=en-US # TTS language
# extpref_tts_rate=175 # TTS speaking rate
# extpref_tts_variant= # TTS voice variant
EOF EOF
close($rc_fh); close($rc_fh);
@@ -327,16 +402,44 @@ EOF
# XDG config directory already set up earlier # XDG config directory already set up earlier
# Check for deprecated -keyf parameter and error out
if (defined($keyf)) {
die("** Error: -keyf is deprecated. Use -keyfile instead.\n** Example: ./ttyverse.pl -keyfile test -oauthwizard\n");
}
# Handle command line argument parsing for keyfile
# Perl's -s flag makes -keyfile without = set $keyfile=1, so fix this
if (defined($keyfile) && $keyfile eq '1' && @ARGV && $ARGV[0] !~ /^-/) {
$keyfile = shift @ARGV; # Take the next argument as the keyfile path
# If it's a relative path, put it in the config directory
if ($keyfile !~ m|^/|) { # Not an absolute path
$keyfile = "$config/$keyfile";
}
# Process remaining flags that Perl's -s might have missed
while (@ARGV && $ARGV[0] =~ /^-(\w+)$/) {
my $flag = $1;
shift @ARGV;
if ($flag eq 'oauthwizard') { $oauthwizard = 1; }
elsif ($flag eq 'retoke') { $retoke = 1; }
elsif ($flag eq 'verbose') { $verbose = 1; }
# Add other flags as needed
}
}
# try to find an OAuth keyfile if we haven't specified key+secret # try to find an OAuth keyfile if we haven't specified key+secret
# no worries if this fails; we could be Basic Auth, after all # no worries if this fails; we could be Basic Auth, after all
$whine = (length($keyf)) ? 1 : 0; my $user_specified_keyfile = length($keyfile) ? 1 : 0;
$keyf ||= "$config/key"; # Only set default keyfile path if none specified
$attempted_keyf = $keyf; if (!$keyfile) {
$keyfile = "$config/key";
}
$whine = $user_specified_keyfile;
$attempted_keyfile = $keyfile;
if (!length($oauthkey) && !length($oauthsecret) # set later if (!length($oauthkey) && !length($oauthsecret) # set later
&& !length($tokenkey) && !length($tokenkey)
&& !length($tokensecret) && !$oauthwizard) { && !length($tokensecret) && !$oauthwizard) {
my $keybuf = ''; my $keybuf = '';
if(open(W, $keyf)) { if(open(W, $keyfile)) {
while(<W>) { while(<W>) {
chomp; chomp;
s/\s+//g; s/\s+//g;
@@ -383,16 +486,16 @@ EOF
$authtype = 'oauth2'; $authtype = 'oauth2';
} }
} }
die("** tried to load OAuth tokens from $keyf\n". die("** tried to load OAuth tokens from $keyfile\n".
" but it seems corrupt or incomplete. please see the documentation,\n". " but it seems corrupt or incomplete. please see the documentation,\n".
" or delete the file so that we can try making your keyfile again.\n") " or delete the file so that we can try making your keyfile again.\n")
unless ($oauth_valid); unless ($oauth_valid);
} else { } else {
die("** couldn't open keyfile $keyf: $!\n". die("** couldn't open keyfile $keyfile: $!\n".
"if you want to run the OAuth wizard to create this file, add ". "if you want to run the OAuth wizard to create this file, add ".
"-oauthwizard\n") "-oauthwizard\n")
if ($whine); if ($whine);
$keyf = ''; # i.e., we loaded nothing from a key file $keyfile = ''; # i.e., we loaded nothing from a key file
} }
} }
@@ -612,6 +715,35 @@ if ($script) {
### now instantiate the TTYverse dynamic API ### ### now instantiate the TTYverse dynamic API ###
### based off the defaults later in script. #### ### based off the defaults later in script. ####
# resolve extension path using XDG directory only
sub resolve_extension_path {
my $name = shift;
# If absolute path, check if it exists
if ($name =~ m{^/}) {
return $name if (-r $name);
return undef;
}
# If relative path (./... or ../...), use as-is if readable
if ($name =~ m{^\.\.?/}) {
return $name if (-r $name);
return undef;
}
# Only location: XDG user extensions directory
my $ext_path = "$data/extensions/$name";
return $ext_path if (-r $ext_path);
# If name doesn't end in .pl, try adding it
unless ($name =~ /\.pl$/) {
my $pl_path = "$data/extensions/$name.pl";
return $pl_path if (-r $pl_path);
}
return undef; # Not found
}
# first we need to load any extensions specified by -exts. # first we need to load any extensions specified by -exts.
if (length($exts) && $exts ne '0') { if (length($exts) && $exts ne '0') {
$multi_module_mode = -1; # mark as loader stage $multi_module_mode = -1; # mark as loader stage
@@ -626,20 +758,26 @@ if (length($exts) && $exts ne '0') {
#TODO #TODO
# wildcards? # wildcards?
$file =~ s/$xstring/,/g; $file =~ s/$xstring/,/g;
print "** loading $file\n" unless ($silent); my $original_file = $file;
# Resolve extension path using XDG + submodule locations
my $extension_path = &resolve_extension_path($file);
die("** extension '$original_file' not found in any search path\n")
unless ($extension_path);
print "** loading $extension_path\n" unless ($silent);
die("** sorry, you cannot load the same extension twice.\n") die("** sorry, you cannot load the same extension twice.\n")
if ($master_store->{$file}->{'loaded'}); if ($master_store->{$original_file}->{'loaded'});
# prepare its working space in $store and load the module # prepare its working space in $store and load the module
$master_store->{$file} = { 'loaded' => 1 }; $master_store->{$original_file} = { 'loaded' => 1 };
$store = \%{ $master_store->{$file} }; $store = \%{ $master_store->{$original_file} };
$EM_DONT_CARE = 0; $EM_DONT_CARE = 0;
$EM_SCRIPT_ON = 1; $EM_SCRIPT_ON = 1;
$EM_SCRIPT_OFF = -1; $EM_SCRIPT_OFF = -1;
$extension_mode = $EM_DONT_CARE; $extension_mode = $EM_DONT_CARE;
die("** $file not found: $!\n") if (! -r "$file"); require $extension_path; # and die if bad
require $file; # and die if bad
die("** $file failed to load: $@\n") if ($@); die("** $file failed to load: $@\n") if ($@);
die("** consistency failure: reference failure on $file\n") die("** consistency failure: reference failure on $file\n")
if (!$store->{'loaded'}); if (!$store->{'loaded'});
@@ -688,6 +826,19 @@ if (length($exts) && $exts ne '0') {
} }
} }
} }
# Show summary of loaded extensions
unless ($silent) {
my @loaded_extensions = ();
foreach my $ext_name (keys %$master_store) {
push @loaded_extensions, $ext_name if $master_store->{$ext_name}->{'loaded'};
}
if (@loaded_extensions) {
my $ext_list = join(', ', @loaded_extensions);
print "** loaded extensions: $ext_list\n";
}
}
# success! enable multi-module support in the TTYverse API and then # success! enable multi-module support in the TTYverse API and then
# dispatch calls through the multi-module system instead. # dispatch calls through the multi-module system instead.
$multi_module_mode = 1; # mark as completed loader $multi_module_mode = 1; # mark as completed loader
@@ -748,6 +899,9 @@ $fetch_id = $last_id || 0;
# we can't do this in BEGIN, because it may not be instantiated yet, # we can't do this in BEGIN, because it may not be instantiated yet,
# and we have to do it after loading modules because it might be in one. # and we have to do it after loading modules because it might be in one.
@notifytypes = (); @notifytypes = ();
# Initialize flag to suppress notifications during initial timeline load
$initial_load_in_progress = 1;
if (length($notifytype) && $notifytype ne '0' && if (length($notifytype) && $notifytype ne '0' &&
$notifytype ne '1' && !$status) { $notifytype ne '1' && !$status) {
# NOT $script! scripts have a use case for notifiers! # NOT $script! scripts have a use case for notifiers!
@@ -1186,7 +1340,7 @@ if ($authtype eq 'basic') {
$tokensecret = undef; $tokensecret = undef;
} }
# but if we are using OAuth, we can request one, unless we are in script # but if we are using OAuth, we can request one, unless we are in script
elsif (($authtype eq 'oauth' || $authtype eq 'oauth2') && (!length($keyf) || $oauthwizard)) { elsif (($authtype eq 'oauth' || $authtype eq 'oauth2') && (!length($keyfile) || $oauthwizard)) {
if (length($oauthkey) && length($oauthsecret) && if (length($oauthkey) && length($oauthsecret) &&
!length($tokenkey) && !length($tokensecret)) { !length($tokenkey) && !length($tokensecret)) {
# we have a key, we don't have the user token # we have a key, we don't have the user token
@@ -1200,7 +1354,7 @@ EOF
exit; exit;
} }
# run the wizard, which writes a keyfile for us # run the wizard, which writes a keyfile for us
$keyf ||= $attempted_keyf; # keyfile is already set correctly from command line or default
print $stdout <<"EOF"; print $stdout <<"EOF";
+----------------------------------------------------------------------------+ +----------------------------------------------------------------------------+
@@ -1215,10 +1369,10 @@ access tokens. This needs to be done JUST ONCE. You can take this keyfile with
you to other systems. If you revoke TTYverse's access, you must remove the you to other systems. If you revoke TTYverse's access, you must remove the
keyfile and start again with a new token. You need to do this once per account keyfile and start again with a new token. You need to do this once per account
you use with TTYverse; only one account token can be stored per keyfile. If you you use with TTYverse; only one account token can be stored per keyfile. If you
have multiple accounts, use -keyf=... to specify different keyfiles. KEEP THESE have multiple accounts, use -keyfile=... to specify different keyfiles. KEEP THESE
FILES SECRET. FILES SECRET.
** This wizard will overwrite $keyf ** This wizard will overwrite $keyfile
Press RETURN/ENTER to continue or CTRL-C NOW! to abort. Press RETURN/ENTER to continue or CTRL-C NOW! to abort.
EOF EOF
$j = <STDIN>; $j = <STDIN>;
@@ -1265,20 +1419,20 @@ EOF
$oauthkey = "X"; $oauthkey = "X";
$oauthsecret = "X"; $oauthsecret = "X";
open(W, ">$keyf") || open(W, ">$keyfile") ||
die("Failed to write keyfile $keyf: $!\n"); die("Failed to write keyfile $keyfile: $!\n");
print W <<"EOF"; print W <<"EOF";
ck=${oauthkey}&cs=${oauthsecret}&at=${tokenkey}&ats=${tokensecret} ck=${oauthkey}&cs=${oauthsecret}&at=${tokenkey}&ats=${tokensecret}
EOF EOF
close(W); close(W);
chmod(0600, $keyf) || print $stdout chmod(0600, $keyfile) || print $stdout
"Warning: could not change permissions on $keyf : $!\n"; "Warning: could not change permissions on $keyfile : $!\n";
print $stdout <<"EOF"; print $stdout <<"EOF";
Written keyfile $keyf Written keyfile $keyfile
Now, restart TTYverse to use this keyfile. Now, restart TTYverse to use this keyfile.
(To choose between multiple keyfiles other than the default .ttyversekey, (To choose between multiple keyfiles other than the default .ttyversekey,
tell TTYverse where the key is using -keyf=... .) tell TTYverse where the key is using -keyfile=... .)
EOF EOF
exit; exit;
@@ -1296,7 +1450,7 @@ EOF
print $streamout <<"EOF"; print $streamout <<"EOF";
you are missing portions of the OAuth sequence. either create a keyfile you are missing portions of the OAuth sequence. either create a keyfile
and point to it with -keyf=... or add these missing pieces: and point to it with -keyfile=... or add these missing pieces:
$error $error
then restart TTYverse, or use -authtype=basic. then restart TTYverse, or use -authtype=basic.
EOF EOF
@@ -1304,7 +1458,7 @@ EOF
} }
} # end OAuth 1.0a else block } # end OAuth 1.0a else block
} }
} elsif ($retoke && length($keyf)) { } elsif ($retoke && length($keyfile)) {
# start the "re-toke" wizard to convert DM-less cloned app keys. # start the "re-toke" wizard to convert DM-less cloned app keys.
# dup STDIN for systems that can only "close" it once # dup STDIN for systems that can only "close" it once
open(STDIN2, "<&STDIN") || die("couldn't dup STDIN: $!\n"); open(STDIN2, "<&STDIN") || die("couldn't dup STDIN: $!\n");
@@ -1329,8 +1483,8 @@ You SHOULD NOT need this wizard if your app key was cloned after 1 June 2011.
However, you can still use it if you experience this specific issue with DMs, However, you can still use it if you experience this specific issue with DMs,
or need to rebuild your keyfile for any other reason. or need to rebuild your keyfile for any other reason.
** This wizard will overwrite the key at $keyf ** This wizard will overwrite the key at $keyfile
** To change this, restart TTYverse with -retoke -keyf=/path/to/keyfile ** To change this, restart TTYverse with -retoke -keyfile=/path/to/keyfile
Press RETURN/ENTER to continue, or CTRL-C NOW! to abort. Press RETURN/ENTER to continue, or CTRL-C NOW! to abort.
EOF EOF
@@ -1441,13 +1595,13 @@ Access token =========> $at
Access token secret ==> $ats Access token secret ==> $ats
EOF EOF
open(W, ">$keyf") || (print $stdout ("Unable to write to $keyf: $!\n"), open(W, ">$keyfile") || (print $stdout ("Unable to write to $keyfile: $!\n"),
exit); exit);
print W "ck=$ck&cs=$cs&at=$at&ats=$ats\n"; print W "ck=$ck&cs=$cs&at=$at&ats=$ats\n";
close(W); close(W);
chmod(0600, $keyf) || print $stdout chmod(0600, $keyfile) || print $stdout
"Warning: could not change permissions on $keyf : $!\n"; "Warning: could not change permissions on $keyfile : $!\n";
print $stdout "Keys written to regenerated keyfile $keyf\n"; print $stdout "Keys written to regenerated keyfile $keyfile\n";
print $stdout "Now restart TTYverse.\n"; print $stdout "Now restart TTYverse.\n";
exit; exit;
} }
@@ -1912,6 +2066,48 @@ sub prinput {
my $i; my $i;
local($_) = shift; # bleh local($_) = shift; # bleh
# Paste protection - detect multi-line input
if (!$script && $_ =~ /\n/) {
my @lines = split(/\n/, $_);
my $line_count = scalar(@lines);
if ($line_count > 3) {
print $stdout "-- PASTE PROTECTION: Detected $line_count lines of input!\n";
print $stdout "-- This looks like an accidental paste.\n";
print $stdout "-- First few lines:\n";
# Show first 3 lines as preview
for my $j (0..($line_count > 3 ? 2 : $line_count-1)) {
my $preview = substr($lines[$j], 0, 60);
$preview .= "..." if length($lines[$j]) > 60;
print $stdout " " . ($j+1) . ": $preview\n";
}
print $stdout "-- Type 'paste' to continue or anything else to cancel: ";
my $response = <STDIN>;
chomp($response);
if (lc($response) ne 'paste') {
print $stdout "-- Multi-line input cancelled.\n";
return 0;
}
print $stdout "-- Processing multi-line input...\n";
}
# Process each line separately with paste protection
my $processed = 0;
for my $line (@lines) {
next if $line =~ /^\s*$/; # Skip empty lines
my $result = &prinput($line);
if ($result < 0) {
return $result; # Propagate quit command (-1)
}
$processed++;
}
return $processed;
}
# validate this string if we are in UTF-8 mode # validate this string if we are in UTF-8 mode
unless ($seven) { unless ($seven) {
$probe = $_; $probe = $_;
@@ -3444,7 +3640,29 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) {
print $stdout "-- no such post (yet?): $code\n"; print $stdout "-- no such post (yet?): $code\n";
return 0; return 0;
} }
my $target = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'screen_name'}); # Use acct field if available, otherwise construct from screen_name and server
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;
}
} 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);
$_ = '@' . $target . " $_"; $_ = '@' . $target . " $_";
unless ($mode eq 'v') { unless ($mode eq 'v') {
$in_reply_to = $post->{'id_str'}; $in_reply_to = $post->{'id_str'};
@@ -3489,7 +3707,28 @@ m#^/(un)?f(boost|a|av|ave|avorite|avourite)? ([zZ]?[a-zA-Z]?[0-9]+)$#) {
print $stdout "-- no such post (yet?): $code\n"; print $stdout "-- no such post (yet?): $code\n";
return 0; return 0;
} }
my $target = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'screen_name'}); # Use acct field if available, otherwise construct from screen_name and server
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;
}
} else {
$target = &descape($post->{'user'}->{'acct'} || $post->{'user'}->{'screen_name'});
}
my $text = $_; my $text = $_;
$_ = '@' . $target; $_ = '@' . $target;
unless ($mode eq 'v') { unless ($mode eq 'v') {
@@ -5405,6 +5644,13 @@ sub refresh {
"-- last_id $last_id, fetch_id $fetch_id, rollback $relative_last_id\n". "-- last_id $last_id, fetch_id $fetch_id, rollback $relative_last_id\n".
"-- (@{[ scalar(keys %id_cache) ]} cached)\n" "-- (@{[ scalar(keys %id_cache) ]} cached)\n"
if ($verbose); if ($verbose);
# Clear initial load flag after first timeline display
if ($initial_load_in_progress) {
$initial_load_in_progress = 0;
print $stdout "-- DEBUG: Initial timeline load complete, notifications now enabled\n" if ($verbose);
}
&send_removereadline if ($termrl); &send_removereadline if ($termrl);
&$conclude; &$conclude;
$wrapseq = 1; $wrapseq = 1;
@@ -5685,22 +5931,59 @@ sub updatest {
return 99; return 99;
} }
# "the pastebrake" # "the pastebrake" - enhanced paste protection
if (!$slowpost && !$verify && !$script) { if (!$slowpost && !$verify && !$script) {
if ((time() - $postbreak_time) < 5) { my $current_time = time();
# Check if posting too fast (more than 2 posts in 3 seconds)
if (($current_time - $postbreak_time) < 3) {
$postbreak_count++; $postbreak_count++;
if ($postbreak_count == 3) {
# First warning after 2 rapid posts
if ($postbreak_count == 2) {
print $stdout print $stdout
"-- you're posting pretty fast. did you mean to do that?\n". "-- PASTE PROTECTION: You're posting very fast!\n".
"-- waiting three seconds before taking the next set of posts\n". "-- This might be an accidental paste. Press CTRL-C to abort!\n".
"-- hit CTRL-C NOW! to kill TTYverse if you accidentally pasted in this window\n"; "-- Waiting 5 seconds... (posts will be ignored during this time)\n";
sleep 3;
$postbreak_count = 0; # Install signal handler for interrupt
local $SIG{INT} = sub {
print $stdout "\n-- PASTE ABORTED by user! No more posts will be sent.\n";
$postbreak_count = 999; # Block further posts
die "User aborted paste sequence\n";
};
# Wait with interrupt checking
for my $i (1..5) {
print $stdout "-- $i... ";
sleep 1;
}
print $stdout "\n-- Continuing (press CTRL-C quickly to abort next posts)\n";
}
# After 4 posts, require confirmation
if ($postbreak_count >= 4) {
print $stdout
"-- PASTE PROTECTION: Blocking rapid posts!\n".
"-- You've posted $postbreak_count times in quick succession.\n".
"-- Type 'continue' to keep posting, or CTRL-C to abort: ";
my $response = <STDIN>;
chomp($response);
if (lc($response) ne 'continue') {
print $stdout "-- Paste sequence aborted by user.\n";
return 98; # Return without posting
}
print $stdout "-- Continuing paste sequence...\n";
$postbreak_count = 0; # Reset after confirmation
} }
} else { } else {
# Reset counter if enough time has passed
$postbreak_count = 0; $postbreak_count = 0;
} }
$postbreak_time = time(); $postbreak_time = $current_time;
} }
my $payload = 'status'; # Always use 'status' for Mastodon my $payload = 'status'; # Always use 'status' for Mastodon
@@ -6018,6 +6301,48 @@ sub standardpost {
$info_line .= " ($relative_time)" if ($relative_time); $info_line .= " ($relative_time)" if ($relative_time);
$info_line .= " via $client_info" if ($client_info); $info_line .= " via $client_info" if ($client_info);
# Add visibility indicator
my $visibility = $ref->{'visibility'} || 'public'; # Default to public if not specified
my $vis_display = '';
my $vis_color = '';
if ($visibility eq 'public') {
$vis_display = '[Public]';
$vis_color = $CYAN; # Subtle cyan for public
} elsif ($visibility eq 'unlisted') {
$vis_display = '[Unlisted]';
$vis_color = $MAGENTA; # Purple for unlisted
} elsif ($visibility eq 'private') {
$vis_display = '[Followers]';
$vis_color = $YELLOW; # Yellow for followers-only
} elsif ($visibility eq 'direct') {
$vis_display = '[Direct]';
$vis_color = $RED; # Red for direct messages
}
if ($vis_display) {
if ($nocolour) {
$info_line .= " $vis_display";
} else {
$info_line .= " ${vis_color}${vis_display}${OFF}";
}
}
# Add content warning/title if present
my $cw_text = '';
if (exists($ref->{'reblog'}) && $ref->{'reblog'} && exists($ref->{'reblog'}->{'spoiler_text'})) {
# For boost posts, check original post's content warning
$cw_text = $ref->{'reblog'}->{'spoiler_text'};
} elsif (exists($ref->{'spoiler_text'})) {
# Regular post content warning
$cw_text = $ref->{'spoiler_text'};
}
if ($cw_text && length($cw_text)) {
$cw_text = &descape($cw_text);
$info_line .= " [$cw_text]";
}
# fediverse doesn't always do this right. # fediverse doesn't always do this right.
$h = $ref->{'reblogs_count'}; $h += 0; #$h = "${h}+" if ($h >= 100); $h = $ref->{'reblogs_count'}; $h += 0; #$h = "${h}+" if ($h >= 100);
# fediverse doesn't always handle single reposts right. good f'n grief. # fediverse doesn't always handle single reposts right. good f'n grief.
@@ -6600,6 +6925,7 @@ sub defaulthandle {
: ''; : '';
print $streamout $menu_select . $dclass . $spost; print $streamout $menu_select . $dclass . $spost;
print $stdout "-- DEBUG: defaulthandle about to call sendnotifies with class='$class'\n" if ($verbose);
&sendnotifies($post_ref, $class); &sendnotifies($post_ref, $class);
return 1; return 1;
} }
@@ -6637,12 +6963,29 @@ sub sendnotifies { # this is a default subroutine of a sort, right?
my $sn = &descape($post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'screen_name'}); my $sn = &descape($post_ref->{'user'}->{'acct'} || $post_ref->{'user'}->{'screen_name'});
my $post = &descape($post_ref->{'text'}); my $post = &descape($post_ref->{'text'});
# interactive? first time? # Debug: Show what we received
unless (length($class) || !$last_id || !length($post)) { print $stdout "-- DEBUG: sendnotifies called with class='$class', sn='$sn'\n" if ($verbose);
# If no class provided, determine it from post content
if (!length($class) && length($post)) {
$class = scalar(&$posttype($post_ref, $sn, $post)); $class = scalar(&$posttype($post_ref, $sn, $post));
print $stdout "-- DEBUG: sendnotifies determined class='$class'\n" if ($verbose);
}
# Debug: Show notify_list status
my $notify_enabled = $notify_list{$class} ? 'YES' : 'NO';
print $stdout "-- DEBUG: notify_list{$class} = $notify_enabled\n" if ($verbose);
# Send notification if we have a valid class, it's enabled, AND initial load is complete
if (length($class) && $notify_list{$class} && !$initial_load_in_progress) {
print $stdout "-- DEBUG: Calling notifytype_dispatch for class='$class'\n" if ($verbose);
&notifytype_dispatch($class, &notifytype_dispatch($class,
&standardpost($post_ref, 1), $post_ref) &standardpost($post_ref, 1), $post_ref);
if ($notify_list{$class}); } else {
my $reason = !length($class) ? "no class" :
!$notify_list{$class} ? "disabled" :
$initial_load_in_progress ? "initial load" : "unknown";
print $stdout "-- DEBUG: NOT calling notifytype_dispatch - class='$class', reason='$reason'\n" if ($verbose);
} }
} }
@@ -6692,7 +7035,7 @@ sub defaultdmhandle {
sub senddmnotifies { sub senddmnotifies {
my $dm_ref = shift; my $dm_ref = shift;
&notifytype_dispatch('DM', &standarddm($dm_ref, 1), $dm_ref) &notifytype_dispatch('dm', &standarddm($dm_ref, 1), $dm_ref)
if ($notify_list{'dm'} && $last_dm); if ($notify_list{'dm'} && $last_dm);
} }
@@ -7316,8 +7659,12 @@ sub tracktags_compile {
# notification multidispatch # notification multidispatch
sub notifytype_dispatch { sub notifytype_dispatch {
print $stdout "-- DEBUG: notifytype_dispatch called with " . scalar(@notifytypes) . " notifiers\n" if ($verbose);
return if (!scalar(@notifytypes)); return if (!scalar(@notifytypes));
my $nt; foreach $nt (@notifytypes) { &$nt(@_); } my $nt; foreach $nt (@notifytypes) {
print $stdout "-- DEBUG: Calling notifier function: $nt\n" if ($verbose);
&$nt(@_);
}
} }
# notifications compiler # notifications compiler
@@ -7330,6 +7677,7 @@ sub notify_compile {
$notify_list{$w} = 1; $notify_list{$w} = 1;
} }
$notifies = join(',', keys %notify_list); $notifies = join(',', keys %notify_list);
print $stdout "-- DEBUG: notify_list compiled: " . join(',', keys %notify_list) . "\n" if ($verbose);
} }
} }
@@ -9141,12 +9489,12 @@ sub oauth2_wizard {
# Step 4: Save credentials to keyfile # Step 4: Save credentials to keyfile
my $keyfile_content = "client_id=${client_id}&client_secret=${client_secret}&access_token=${access_token}&server=${fediverseserver}"; my $keyfile_content = "client_id=${client_id}&client_secret=${client_secret}&access_token=${access_token}&server=${fediverseserver}";
open(W, ">$keyf") || die("couldn't write keyfile $keyf: $!\n"); open(W, ">$keyfile") || die("couldn't write keyfile $keyfile: $!\n");
chmod 0600, $keyf; chmod 0600, $keyfile;
print W $keyfile_content; print W $keyfile_content;
close(W); close(W);
print $stdout "\nKeyfile $keyf written successfully!\n"; print $stdout "\nKeyfile $keyfile written successfully!\n";
print $stdout "You can now use TTYverse with your $fediverseserver account.\n"; print $stdout "You can now use TTYverse with your $fediverseserver account.\n";
exit; exit;