diff --git a/ttyverse.pl b/ttyverse.pl index 44519ff..3c06e58 100755 --- a/ttyverse.pl +++ b/ttyverse.pl @@ -2566,6 +2566,8 @@ Example: /url a5 - opens all URLs in post a5 /delete a5 - deletes post a5, if it's your post /boost a5 - boosts post a5 + /vote a5 2 - vote for option 2 on poll in post a5 + /vote a5 1,3 - vote for options 1 and 3 (if multiple choice) Abbreviations: /re, /th, /url, /del Menu codes wrap around at end. @@ -3566,6 +3568,13 @@ EOF return &common_split_post($_, undef, $1); } + # vote on polls + if (m#^/vote\s+([a-z]\d+)\s+(.+)$#i) { + my $post_code = $1; + my $choices = $2; + return &vote_on_poll($post_code, $choices); + } + # follow and leave users if (m#^/(follow|leave|unfollow) \@?([^\s/]+)$#) { my $m = $1; @@ -3935,6 +3944,95 @@ sub common_split_post { return 1; } +# vote on a poll +sub vote_on_poll { + my $post_code = shift; + my $choices = shift; + + # Find the post by its menu code + my $post_ref = &findtarget($post_code); + unless ($post_ref) { + print $stdout "-- no such post: $post_code\n"; + return 0; + } + + # Check if post has a poll + unless (exists($post_ref->{'poll'}) && $post_ref->{'poll'}) { + print $stdout "-- post $post_code does not have a poll\n"; + return 0; + } + + my $poll = $post_ref->{'poll'}; + + # Check if poll is expired or already voted + if ($poll->{'expired'}) { + print $stdout "-- poll has expired\n"; + return 0; + } + + if ($poll->{'voted'}) { + print $stdout "-- you have already voted on this poll\n"; + return 0; + } + + # Parse choices (support both single numbers and comma-separated) + my @choice_nums = split(/[\s,]+/, $choices); + my @valid_choices = (); + my $max_options = scalar(@{$poll->{'options'}}); + + foreach my $choice (@choice_nums) { + $choice =~ s/\D//g; # remove non-digits + if ($choice < 1 || $choice > $max_options) { + print $stdout "-- invalid choice: $choice (must be 1-$max_options)\n"; + return 0; + } + push @valid_choices, ($choice - 1); # Convert to 0-based index + } + + unless (@valid_choices) { + print $stdout "-- no valid choices specified\n"; + return 0; + } + + # Check if multiple choices are allowed + if (!$poll->{'multiple'} && scalar(@valid_choices) > 1) { + print $stdout "-- this poll only allows single choice\n"; + return 0; + } + + # Submit the vote + my $post_id = $post_ref->{'id_str'} || $post_ref->{'id'}; + my $vote_url = "$apibase/polls/$poll->{'id'}/votes"; + + # Build POST data with choices + my $post_data = ''; + foreach my $choice_idx (@valid_choices) { + $post_data .= "&choices[]=$choice_idx"; + } + $post_data =~ s/^&//; # remove leading & + + print $stdout "-- submitting vote on poll...\n"; + + my $result = &backticks($useragent, '/dev/null', undef, $vote_url, $post_data, 1, @agentopts); + + if ($result) { + print $stdout "-- vote submitted successfully\n"; + + # Show which options were selected + my @chosen_titles = (); + foreach my $choice_idx (@valid_choices) { + my $title = $poll->{'options'}->[$choice_idx]->{'title'} || ''; + push @chosen_titles, ($choice_idx + 1) . ". $title"; + } + print $stdout "-- voted for: " . join(', ', @chosen_titles) . "\n"; + + return 1; + } else { + print $stdout "-- failed to submit vote\n"; + return 0; + } +} + # helper functions for the command line processor. sub add_history { my $h = shift; @@ -5721,6 +5819,12 @@ sub standardpost { my $booster = &descape($ref->{'boost_attribution'}); $post = "[boosted $booster] $post"; } + + # Add poll information if this post has a poll + if (exists($ref->{'poll'}) && $ref->{'poll'}) { + my $poll_info = &format_poll_display($ref->{'poll'}); + $post .= "\n$poll_info" if ($poll_info); + } my $colour; my $g; my $h; @@ -5789,6 +5893,50 @@ s/(^|[^a-zA-Z0-9_])\@([a-zA-Z0-9_\/]+)/\1\@${UNDER}\2${colour}/g; return $post; } +# format poll information for display +sub format_poll_display { + my $poll = shift; + return '' unless ($poll && ref($poll) eq 'HASH'); + + my @options = @{$poll->{'options'} || []}; + return '' unless (@options); + + my $poll_text = "Poll:"; + my $option_num = 1; + + # Display each option with number and vote count + foreach my $option (@options) { + my $title = &descape($option->{'title'} || ''); + my $votes = $option->{'votes_count'} || 0; + $poll_text .= "\n $option_num. $title ($votes votes)"; + $option_num++; + } + + # Add poll metadata + my $total_votes = $poll->{'votes_count'} || 0; + my $expires_at = $poll->{'expires_at'}; + my $expired = $poll->{'expired'} || 0; + my $multiple = $poll->{'multiple'} || 0; + my $voted = $poll->{'voted'} || 0; + + $poll_text .= "\n Total: $total_votes votes"; + $poll_text .= $multiple ? " (multiple choice)" : " (single choice)"; + + if ($expired) { + $poll_text .= " - EXPIRED"; + } elsif ($expires_at) { + $poll_text .= " - expires $expires_at"; + } + + if ($voted) { + $poll_text .= " - Already voted"; + } else { + $poll_text .= " - Use /vote to participate"; + } + + return $poll_text; +} + # format a DM based on standard user options sub standarddm { my $ref = shift; @@ -7664,6 +7812,24 @@ sub normalizejson { print $stdout "-- DEBUG: Final boost data applied - text: '$boost_content', attribution: '$boost_attribution'\n" if ($verbose); } + # Handle poll data - check for polls in boost-aware manner (Bifrost pattern) + my $poll_data = undef; + if (exists($i->{'reblog'}) && exists($i->{'reblog'}->{'poll'})) { + # Poll is in the boosted post + $poll_data = $i->{'reblog'}->{'poll'}; + print $stdout "-- DEBUG: Found poll in boosted post\n" if ($verbose); + } elsif (exists($i->{'poll'})) { + # Poll is in the main post + $poll_data = $i->{'poll'}; + print $stdout "-- DEBUG: Found poll in main post\n" if ($verbose); + } + + # Store poll data for display + if ($poll_data) { + $i->{'poll'} = $poll_data; + print $stdout "-- DEBUG: Poll data stored for display\n" if ($verbose); + } + return $i; }