Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env perl
- use strict;
- use warnings;
- use IO::Socket::INET;
- use LWP::UserAgent;
- use JSON;
- # === CONFIGURATION ===
- my $host = '8bit.fansi.org';
- my $port = 4201;
- my $bot_name = 'ChatBot';
- my $bot_password = 'xxxxxxxx';
- my $api_key = xxxxxxxx
- my $MAX_TURNS = 10;
- my $HOURS_24 = 60 * 60 * 24;
- # === IN-MEMORY STATE ===
- my %history;
- my %recent_users;
- # === CONNECT TO SERVER ===
- my $sock = IO::Socket::INET->new(
- PeerAddr => $host,
- PeerPort => $port,
- Proto => 'tcp'
- ) or die "Can't connect to $host:$port – $!";
- print "Connected.\n";
- sleep 1;
- print $sock "connect $bot_name $bot_password\n";
- # === MAIN LOOP ===
- while (my $line = <$sock>) {
- print ">> $line";
- prune_recent_users();
- if ($line =~ /<Public>\s+.*?([A-Za-z0-9_-]+) says, "\+ai (.+)"/i) {
- my ($speaker, $prompt) = ($1, $2);
- next if lc $speaker eq lc $bot_name;
- print "Prompt from $speaker: $prompt\n";
- my $reply = process_prompt($speaker, $prompt);
- next unless defined $reply;
- $reply = sanitize_for_mush($reply);
- send_to_mush("+public $reply");
- }
- }
- # === SUBROUTINES ===
- sub send_to_mush {
- my ($cmd) = @_;
- print $sock "$cmd\n";
- print "Sent: $cmd\n";
- }
- sub process_prompt {
- my ($speaker, $prompt) = @_;
- if ($prompt =~ /
- \b
- (
- (who.*?(talking|chatting|active)) |
- (are\s+you.*?(chatting|talking)) |
- (is\s+anyone.*?(chatting|talking)) |
- (anyone\s+(chatting|talking)) |
- (who'?s\s+(chatting|talking|active))
- )
- /ix) {
- my @names = sort keys %recent_users;
- my $list = @names ? join(', ', @names) : 'nobody in the last 24 hours';
- save_recent_users_attr();
- return "I've chatted with: $list";
- }
- return get_openai_response($speaker, $prompt);
- }
- sub get_openai_response {
- my ($speaker, $prompt) = @_;
- my $ua = LWP::UserAgent->new;
- push @{ $history{$speaker} }, { role => 'user', content => $prompt };
- splice @{ $history{$speaker} }, 0, @{ $history{$speaker} } - $MAX_TURNS * 2
- if @{ $history{$speaker} } > $MAX_TURNS * 2;
- my @messages = (
- { role => 'system', content =>
- 'You are a social bot willing to befriend anyone who is polite. '
- . 'Feel free to clap back if warranted, but stay edgy without breaking laws.' },
- @{ $history{$speaker} },
- );
- my $res = $ua->post(
- 'https://api.openai.com/v1/chat/completions',
- 'Content-Type' => 'application/json',
- 'Authorization' => "Bearer $api_key",
- Content => encode_json({
- model => 'gpt-4o-mini',
- messages => \@messages,
- max_tokens => 1000,
- }),
- );
- unless ($res->is_success) {
- warn "OpenAI error: ".$res->status_line;
- return undef;
- }
- my $data = decode_json($res->decoded_content);
- my $reply = $data->{choices}[0]{message}{content} // '(no response)';
- push @{ $history{$speaker} }, { role => 'assistant', content => $reply };
- $recent_users{$speaker} = time;
- set_history_on_mush($speaker);
- save_recent_users_attr();
- return $reply;
- }
- sub set_history_on_mush {
- my ($speaker) = @_;
- return unless exists $history{$speaker};
- my @lines;
- for my $msg (@{ $history{$speaker} }) {
- my $role = ucfirst $msg->{role};
- my $text = sanitize_for_mush($msg->{content});
- push @lines, "$role: $text";
- }
- my $joined = join('%r', @lines);
- substr($joined, 8192) = '' if length($joined) > 8192;
- send_to_mush("\@set me=" . lc($speaker) . ":$joined");
- }
- sub save_recent_users_attr {
- my @names = sort keys %recent_users;
- my $list = join(' ', @names);
- send_to_mush("\@set me=recent_users:$list");
- }
- sub prune_recent_users {
- my $now = time;
- delete $recent_users{$_}
- for grep { $now - $recent_users{$_} > $HOURS_24 } keys %recent_users;
- }
- sub sanitize_for_mush {
- my ($text) = @_;
- # Normalize punctuation
- $text =~ s/[\x{2018}\x{2019}\x{201B}]/'/g; # curly apostrophes
- $text =~ s/[\x{201C}\x{201D}\x{201E}]/"/g; # curly quotes
- $text =~ s/\x{2013}/-/g; # en dash
- $text =~ s/\x{2014}/-/g; # em dash
- $text =~ s/\x{2026}/.../g; # ellipsis
- # Remove corrupted emoji and mojibake like 🌭 or similar
- $text =~ s/ΓÇö/-/g;
- $text =~ s/≡ƒ[^\s]*//g; # common mojibake pattern for emoji
- $text =~ s/[^\x00-\x7F]//g; # remove all non-ASCII chars
- # Clean up for PennMUSH display
- $text =~ s/[\r\n]+/ /g;
- $text =~ s/[\[\]]/\\$&/g;
- return $text;
- }
Advertisement
Add Comment
Please, Sign In to add comment