Toobis

Untitled

Jul 15th, 2025
11
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 4.56 KB | None | 0 0
  1. #!/usr/bin/env perl
  2. use strict;
  3. use warnings;
  4. use IO::Socket::INET;
  5. use LWP::UserAgent;
  6. use JSON;
  7.  
  8. # === CONFIGURATION ===
  9. my $host = '8bit.fansi.org';
  10. my $port = 4201;
  11. my $bot_name = 'ChatBot';
  12. my $bot_password = 'xxxxxxxx';
  13. my $api_key = xxxxxxxx
  14. my $MAX_TURNS = 10;
  15. my $HOURS_24 = 60 * 60 * 24;
  16.  
  17. # === IN-MEMORY STATE ===
  18. my %history;
  19. my %recent_users;
  20.  
  21. # === CONNECT TO SERVER ===
  22. my $sock = IO::Socket::INET->new(
  23. PeerAddr => $host,
  24. PeerPort => $port,
  25. Proto => 'tcp'
  26. ) or die "Can't connect to $host:$port – $!";
  27.  
  28. print "Connected.\n";
  29. sleep 1;
  30. print $sock "connect $bot_name $bot_password\n";
  31.  
  32. # === MAIN LOOP ===
  33. while (my $line = <$sock>) {
  34. print ">> $line";
  35. prune_recent_users();
  36.  
  37. if ($line =~ /<Public>\s+.*?([A-Za-z0-9_-]+) says, "\+ai (.+)"/i) {
  38. my ($speaker, $prompt) = ($1, $2);
  39. next if lc $speaker eq lc $bot_name;
  40.  
  41. print "Prompt from $speaker: $prompt\n";
  42. my $reply = process_prompt($speaker, $prompt);
  43. next unless defined $reply;
  44.  
  45. $reply = sanitize_for_mush($reply);
  46. send_to_mush("+public $reply");
  47. }
  48. }
  49.  
  50. # === SUBROUTINES ===
  51.  
  52. sub send_to_mush {
  53. my ($cmd) = @_;
  54. print $sock "$cmd\n";
  55. print "Sent: $cmd\n";
  56. }
  57.  
  58. sub process_prompt {
  59. my ($speaker, $prompt) = @_;
  60.  
  61. if ($prompt =~ /
  62. \b
  63. (
  64. (who.*?(talking|chatting|active)) |
  65. (are\s+you.*?(chatting|talking)) |
  66. (is\s+anyone.*?(chatting|talking)) |
  67. (anyone\s+(chatting|talking)) |
  68. (who'?s\s+(chatting|talking|active))
  69. )
  70. /ix) {
  71.  
  72. my @names = sort keys %recent_users;
  73. my $list = @names ? join(', ', @names) : 'nobody in the last 24 hours';
  74. save_recent_users_attr();
  75. return "I've chatted with: $list";
  76. }
  77.  
  78. return get_openai_response($speaker, $prompt);
  79. }
  80.  
  81. sub get_openai_response {
  82. my ($speaker, $prompt) = @_;
  83. my $ua = LWP::UserAgent->new;
  84.  
  85. push @{ $history{$speaker} }, { role => 'user', content => $prompt };
  86. splice @{ $history{$speaker} }, 0, @{ $history{$speaker} } - $MAX_TURNS * 2
  87. if @{ $history{$speaker} } > $MAX_TURNS * 2;
  88.  
  89. my @messages = (
  90. { role => 'system', content =>
  91. 'You are a social bot willing to befriend anyone who is polite. '
  92. . 'Feel free to clap back if warranted, but stay edgy without breaking laws.' },
  93. @{ $history{$speaker} },
  94. );
  95.  
  96. my $res = $ua->post(
  97. 'https://api.openai.com/v1/chat/completions',
  98. 'Content-Type' => 'application/json',
  99. 'Authorization' => "Bearer $api_key",
  100. Content => encode_json({
  101. model => 'gpt-4o-mini',
  102. messages => \@messages,
  103. max_tokens => 1000,
  104. }),
  105. );
  106.  
  107. unless ($res->is_success) {
  108. warn "OpenAI error: ".$res->status_line;
  109. return undef;
  110. }
  111.  
  112. my $data = decode_json($res->decoded_content);
  113. my $reply = $data->{choices}[0]{message}{content} // '(no response)';
  114.  
  115. push @{ $history{$speaker} }, { role => 'assistant', content => $reply };
  116. $recent_users{$speaker} = time;
  117.  
  118. set_history_on_mush($speaker);
  119. save_recent_users_attr();
  120. return $reply;
  121. }
  122.  
  123. sub set_history_on_mush {
  124. my ($speaker) = @_;
  125. return unless exists $history{$speaker};
  126.  
  127. my @lines;
  128. for my $msg (@{ $history{$speaker} }) {
  129. my $role = ucfirst $msg->{role};
  130. my $text = sanitize_for_mush($msg->{content});
  131. push @lines, "$role: $text";
  132. }
  133.  
  134. my $joined = join('%r', @lines);
  135. substr($joined, 8192) = '' if length($joined) > 8192;
  136.  
  137. send_to_mush("\@set me=" . lc($speaker) . ":$joined");
  138. }
  139.  
  140. sub save_recent_users_attr {
  141. my @names = sort keys %recent_users;
  142. my $list = join(' ', @names);
  143. send_to_mush("\@set me=recent_users:$list");
  144. }
  145.  
  146. sub prune_recent_users {
  147. my $now = time;
  148. delete $recent_users{$_}
  149. for grep { $now - $recent_users{$_} > $HOURS_24 } keys %recent_users;
  150. }
  151.  
  152. sub sanitize_for_mush {
  153. my ($text) = @_;
  154.  
  155. # Normalize punctuation
  156. $text =~ s/[\x{2018}\x{2019}\x{201B}]/'/g; # curly apostrophes
  157. $text =~ s/[\x{201C}\x{201D}\x{201E}]/"/g; # curly quotes
  158. $text =~ s/\x{2013}/-/g; # en dash
  159. $text =~ s/\x{2014}/-/g; # em dash
  160. $text =~ s/\x{2026}/.../g; # ellipsis
  161.  
  162. # Remove corrupted emoji and mojibake like 🌭 or similar
  163. $text =~ s/ΓÇö/-/g;
  164. $text =~ s/≡ƒ[^\s]*//g; # common mojibake pattern for emoji
  165. $text =~ s/[^\x00-\x7F]//g; # remove all non-ASCII chars
  166.  
  167. # Clean up for PennMUSH display
  168. $text =~ s/[\r\n]+/ /g;
  169. $text =~ s/[\[\]]/\\$&/g;
  170.  
  171. return $text;
  172. }
  173.  
Advertisement
Add Comment
Please, Sign In to add comment