2024-12-16 03:36:08 +00:00
# {
# 'speciesCode': 'dowwoo',
# 'comName': 'Downy Woodpecker',
# 'sciName': 'Dryobates pubescens',
# 'locId': 'L36986367',
# 'locName': 'Home',
# 'obsDt': '2024-12-14 17:06',
# 'howMany': 1,
# 'lat': 36.0006572,
# 'lng': -95.0865753,
# 'obsValid': True,
# 'obsReviewed': False,
# 'locationPrivate': True,
# 'subId': 'S205413945'
# }
2024-12-15 05:49:47 +00:00
from random import choice
import requests
from time import sleep
import socket
import re
from json import dump , load , dumps
try :
2024-12-15 07:00:05 +00:00
with open ( " config.json " , " r " ) as f :
config = load ( f )
2024-12-15 05:49:47 +00:00
except FileNotFoundError :
2024-12-15 07:00:05 +00:00
exit ( " Please create config.json with api key(s) and channels " )
2024-12-15 05:49:47 +00:00
host = " localhost "
port = 6667
2024-12-15 07:00:05 +00:00
nick = config [ " nick " ]
2024-12-15 05:49:47 +00:00
realname = " a bot by ~nebula "
2024-12-16 03:36:08 +00:00
helptext = " !birds, !trivia, !trscores, !aitrivia, !aiscores. contact ~nebula for help, feedback or problem reports. https://git.tilde.town/nebula/mysterious_cube "
2024-12-15 07:00:05 +00:00
channels = config [ " channels " ]
2024-12-15 05:49:47 +00:00
2024-12-15 21:06:47 +00:00
channel_re = re . compile ( r " PRIVMSG (#* \ w+) " )
name_re = re . compile ( r " ^:([^!]*)! " )
llm_answer_re = re . compile ( r " ^(true|false) " )
llama_url = " https://llama.mcopp.com/v1/chat/completions "
llama_headers = {
" Content-Type " : " application/json " ,
" Authorization " : config [ " llama_key " ]
}
2024-12-16 03:36:08 +00:00
geonames_url = " http://api.geonames.org/searchJSON "
geonames_user = config [ " geonames_user " ]
ebird_url = " https://api.ebird.org/v2/data/obs/geo/recent "
ebird_key = config [ " ebird_key " ]
google_url = " https://customsearch.googleapis.com/customsearch/v1 "
google_key = config [ " google_key " ]
google_cx = config [ " google_cx " ]
2024-12-15 21:06:47 +00:00
trivia_questions_file = " trivia.questions "
trivia_state_file = " trivia.state "
trivia_score_file = " trivia.scores "
trivia_unselected_file = " trivia.unselected "
2024-12-15 05:49:47 +00:00
ai_state_file = " trivia.aistate "
ai_score_file = " trivia.aiscores "
2024-12-16 03:36:08 +00:00
bird_url_file = " brids.urls "
2024-12-15 05:49:47 +00:00
try :
2024-12-15 21:06:47 +00:00
with open ( trivia_questions_file , " r " ) as f :
trivia_questions = load ( f )
2024-12-15 05:49:47 +00:00
except FileNotFoundError :
2024-12-15 21:06:47 +00:00
trivia_questions = [ ]
2024-12-15 05:49:47 +00:00
try :
2024-12-15 21:06:47 +00:00
with open ( trivia_state_file , " r " ) as f :
trivia_state = load ( f )
2024-12-15 05:49:47 +00:00
except FileNotFoundError :
2024-12-15 21:06:47 +00:00
trivia_state = { }
2024-12-15 05:49:47 +00:00
try :
2024-12-15 21:06:47 +00:00
with open ( trivia_score_file , " r " ) as f :
trivia_scores = load ( f )
2024-12-15 05:49:47 +00:00
except FileNotFoundError :
2024-12-15 21:06:47 +00:00
trivia_scores = { }
2024-12-15 05:49:47 +00:00
try :
2024-12-15 21:06:47 +00:00
with open ( trivia_unselected_file , " r " ) as f :
trivia_unselected = load ( f )
2024-12-15 05:49:47 +00:00
except FileNotFoundError :
2024-12-15 21:06:47 +00:00
trivia_unselected = [ ]
2024-12-15 05:49:47 +00:00
try :
with open ( ai_state_file , " r " ) as f :
ai_state = load ( f )
except FileNotFoundError :
ai_state = { }
try :
with open ( ai_score_file , " r " ) as f :
ai_scores = load ( f )
except FileNotFoundError :
ai_scores = { }
2024-12-16 03:36:08 +00:00
try :
with open ( bird_url_file , " r " ) as f :
bird_urls = load ( f )
except FileNotFoundError :
bird_urls = { }
2024-12-15 05:49:47 +00:00
def write_state ( ) :
2024-12-15 21:06:47 +00:00
with open ( trivia_state_file , " w " ) as f :
dump ( trivia_state , f )
with open ( trivia_score_file , " w " ) as f :
dump ( trivia_scores , f )
with open ( trivia_unselected_file , " w " ) as f :
dump ( trivia_unselected , f )
2024-12-15 05:49:47 +00:00
with open ( ai_score_file , " w " ) as f :
dump ( ai_scores , f )
with open ( ai_state_file , " w " ) as f :
dump ( ai_state , f )
2024-12-16 03:36:08 +00:00
with open ( bird_url_file , " w " ) as f :
dump ( bird_urls , f )
def get_location ( query ) :
params = {
" username " : geonames_user ,
" q " : query ,
" maxRows " : 1
}
try :
response = requests . get ( geonames_url , params = params )
data = response . json ( ) [ " geonames " ] [ 0 ]
if " lat " not in data :
return None
else :
return data
except IndexError :
return None
def cache_bird_url ( sciName ) :
global bird_urls
if sciName in bird_urls . keys ( ) :
return bird_urls [ sciName ]
search = requests . get ( google_url , params = {
" q " : sciName ,
" key " : google_key ,
" cx " : google_cx
} )
data = search . json ( )
try :
link = data [ " items " ] [ 0 ] [ " link " ]
bird_urls [ sciName ] = link
write_state ( )
return link
except ( IndexError , KeyError ) :
return None
def get_birds ( location ) :
params = {
" key " : ebird_key ,
" dist " : 50 ,
" lat " : location [ " lat " ] ,
" lng " : location [ " lng " ]
}
request = requests . get ( ebird_url , params = params )
data = request . json ( ) [ : 4 ]
line = f " Location: { location [ ' name ' ] } ; "
for sighting in data :
url = cache_bird_url ( sighting [ ' sciName ' ] ) . rstrip ( " /id " )
line + = f " { sighting [ ' comName ' ] } [ { url } ]; "
return line . rstrip ( " ; " )
def post_birds ( channel , username , arguments ) :
if not arguments :
return " Posts recently sighted birds. Give a location name with this command, eg !birds Dodge City, KS "
location = get_location ( arguments )
if not location :
return f " No data found for { arguments } "
birds = get_birds ( location )
if not birds :
return f " No data found for { arguments } "
return birds
2024-12-15 05:49:47 +00:00
def get_question ( ai_enabled = False ) :
2024-12-15 21:06:47 +00:00
global trivia_questions
global trivia_unselected
if trivia_questions :
if not trivia_unselected :
trivia_unselected = trivia_questions . copy ( )
question = choice ( trivia_unselected )
trivia_unselected . remove ( question )
2024-12-15 05:49:47 +00:00
question . append ( ai_enabled )
# print(len(unselected))
return question
else :
return False
2024-12-15 22:07:30 +00:00
def post_question ( channel , username , arguments ) :
2024-12-15 21:06:47 +00:00
global trivia_state
2024-12-15 05:49:47 +00:00
question = get_question ( )
if question :
2024-12-15 21:06:47 +00:00
trivia_state [ channel ] = question
2024-12-15 05:49:47 +00:00
write_state ( )
return f " Answer ' true ' or ' false ' : { question [ 0 ] } "
else :
return " internal error "
2024-12-15 22:07:30 +00:00
def post_ai_question ( channel , username , arguments ) :
2024-12-15 05:49:47 +00:00
global ai_state
question = get_question ( ai_enabled = True )
if question :
ai_state [ channel ] = question
write_state ( )
return f " Will AI answer this true/false statement ' right ' or ' wrong ' : { question [ 0 ] } "
else :
return " internal error "
def ai_answer ( choice , channel , name ) :
global ai_state
if channel not in ai_state . keys ( ) :
return None
question_text , answer , ai_enabled = ai_state [ channel ]
user_correct = False
try :
llm_response = llama_response ( question_text )
llm_answer = llm_answer_re . search ( llm_response . lower ( ) )
except Exception as e :
print ( e )
return " internal error "
del ai_state [ channel ]
write_state ( )
if llm_answer :
llm_answer = llm_answer . group ( 1 )
if llm_answer . lower ( ) == answer :
line = " The AI was (at least kind of) right! "
2024-12-15 19:17:47 +00:00
user_correct = choice == " right "
2024-12-15 05:49:47 +00:00
else :
line = " The AI was wrong! "
2024-12-15 19:17:47 +00:00
user_correct = choice == " wrong "
2024-12-15 05:49:47 +00:00
else :
return [
f " Cannot automatically determine if AI is right or wrong. " ,
f " AI Response: { llm_response } " ,
f " The right answer is { answer } ! "
]
# print(f"{answer}; {choice}; {user_correct}")
if name :
if name not in ai_scores . keys ( ) :
ai_scores [ name ] = 0
if user_correct :
ai_scores [ name ] + = 1
write_state ( )
return [
f " AI response: { llm_response } " ,
line + f " { name } scores 1 AI point! Total AI score for { name } : { ai_scores [ name ] } pts. See top AI scores with !aiscores "
]
else :
ai_scores [ name ] - = 1
write_state ( )
return [
f " AI response: { llm_response } " ,
line + f " { name } loses 1 AI point! Total AI score for { name } : { ai_scores [ name ] } pts. See top AI scores with !aiscores "
]
return [
f " AI response: { llm_response } " ,
f " The right answer is { answer } ! "
]
def answer ( choice , channel , name ) :
2024-12-15 21:06:47 +00:00
global trivia_state
if channel not in trivia_state . keys ( ) :
2024-12-15 05:49:47 +00:00
return None
2024-12-15 21:06:47 +00:00
_ , answer , ai_enabled = trivia_state [ channel ]
del trivia_state [ channel ]
2024-12-15 05:49:47 +00:00
write_state ( )
line = f " The answer is { answer } ! "
if not ai_enabled and name :
2024-12-15 21:06:47 +00:00
if name not in trivia_scores . keys ( ) :
trivia_scores [ name ] = 0
2024-12-15 05:49:47 +00:00
if choice == answer :
2024-12-15 21:06:47 +00:00
trivia_scores [ name ] + = 1
line + = f " { name } scores 1 point! Total score for { name } : { trivia_scores [ name ] } pts. "
2024-12-15 05:49:47 +00:00
else :
2024-12-15 21:06:47 +00:00
trivia_scores [ name ] - = 1
line + = f " { name } loses 1 point! Total score for { name } : { trivia_scores [ name ] } pts. "
2024-12-15 05:49:47 +00:00
write_state ( )
line + = " See top scores with !trscores "
return line
2024-12-15 22:07:30 +00:00
def post_top_scores ( channel , name , arguments ) :
2024-12-15 21:06:47 +00:00
global trivia_scores
score_list = [ ( name , score ) for name , score in trivia_scores . items ( ) ]
2024-12-15 05:49:47 +00:00
if not score_list :
return " No current scores. "
sorted_scores = sorted ( score_list , key = lambda x : x [ 1 ] , reverse = True )
line = " Top scores: "
count = 1
for name , score in sorted_scores :
if count > 10 :
break
line + = f " [ { count } . { make_no_ping_username ( name ) } : { score } pts], "
count + = 1
return line [ : - 2 ]
2024-12-15 22:07:30 +00:00
def post_top_ai_scores ( channel , name , arguments ) :
2024-12-15 05:49:47 +00:00
global ai_scores
score_list = [ ( name , score ) for name , score in ai_scores . items ( ) ]
if not score_list :
return " No current AI scores. "
sorted_scores = sorted ( score_list , key = lambda x : x [ 1 ] , reverse = True )
line = " Top AI scores: "
count = 1
for name , score in sorted_scores :
if count > 10 :
break
line + = f " [ { count } . { make_no_ping_username ( name ) } : { score } pts], "
count + = 1
return line [ : - 2 ]
2024-12-15 22:07:30 +00:00
def post_help ( channel , name , arguments ) :
2024-12-15 21:06:47 +00:00
return helptext
2024-12-15 22:07:30 +00:00
def answer_true ( channel , name , arguments ) :
2024-12-15 05:49:47 +00:00
return answer ( " true " , channel , name )
2024-12-15 22:07:30 +00:00
def answer_false ( channel , name , arguments ) :
2024-12-15 05:49:47 +00:00
return answer ( " false " , channel , name )
2024-12-15 22:07:30 +00:00
def answer_right ( channel , name , arguments ) :
2024-12-15 19:17:47 +00:00
return ai_answer ( " right " , channel , name )
2024-12-15 05:49:47 +00:00
2024-12-15 22:07:30 +00:00
def answer_wrong ( channel , name , arguments ) :
2024-12-15 19:17:47 +00:00
return ai_answer ( " wrong " , channel , name )
2024-12-15 05:49:47 +00:00
def make_no_ping_username ( name ) :
return name [ 0 ] + " \u200b " + name [ 1 : ]
def llama_response ( question ) :
content = {
" n_predict " : 64 ,
" temperature " : 0.6 ,
" min_p " : 0.05 ,
" messages " : [
{
" role " : " system " ,
" content " : " You are an entertaining bot in an IRC server. Your responses are brief. "
} ,
{
" role " : " user " ,
" content " : f " { question } True, or false? Briefly explain why. "
}
]
}
r = requests . post ( llama_url , headers = llama_headers , data = dumps ( content ) )
response = r . json ( )
return response [ " choices " ] [ 0 ] [ " message " ] [ " content " ]
class IRCBot ( ) :
2024-12-15 07:00:05 +00:00
def __init__ ( self , nick , realname , helptext , commands , searchers , channels ) :
2024-12-15 05:49:47 +00:00
self . s = socket . socket ( socket . AF_INET , socket . SOCK_STREAM )
self . s . connect ( ( host , port ) )
self . nick = nick
self . realname = realname
self . helptext = helptext
self . commands = commands
2024-12-15 07:00:05 +00:00
self . searchers = searchers
2024-12-15 05:49:47 +00:00
self . channels = channels
self . sendline ( f " NICK { self . nick } " )
self . sendline ( f " USER { self . nick } 0 * : { self . realname } " )
for channel in self . channels :
self . sendline ( f " JOIN { channel } " )
def sendline ( self , line ) :
2024-12-16 03:36:08 +00:00
if line :
return self . s . send ( bytes ( f " { line } \r \n " , " UTF-8 " ) )
return None
2024-12-15 05:49:47 +00:00
def send ( self , channel , content ) :
if isinstance ( content , list ) :
for line in content :
self . sendline ( f " PRIVMSG { channel } : { line } " )
sleep ( 0.5 )
elif isinstance ( content , str ) :
self . sendline ( f " PRIVMSG { channel } : { content } " )
2024-12-15 22:07:30 +00:00
def command_loop ( self ) :
2024-12-15 05:49:47 +00:00
while True :
2024-12-15 21:42:14 +00:00
char = self . s . recv ( 1 )
2024-12-15 21:32:00 +00:00
if not char :
2024-12-15 19:17:47 +00:00
exit ( f " { self . nick } : no response from IRC server " )
2024-12-15 21:42:14 +00:00
line = b " "
2024-12-15 22:07:30 +00:00
while char != b " \n " :
if char != b " \r " :
2024-12-15 21:32:00 +00:00
line + = char
2024-12-15 21:42:14 +00:00
char = self . s . recv ( 1 )
line = line . decode ( " UTF-8 " ) . strip ( )
2024-12-15 21:32:00 +00:00
if line . startswith ( " PING " ) :
pong = " PONG " + line [ 5 : ]
self . sendline ( pong )
continue
channel_search = channel_re . search ( line )
if not channel_search :
continue
channel = channel_search . group ( 1 )
name_search = name_re . search ( line )
if name_search :
name = name_search . group ( 1 )
else :
name = None
if name and not channel . startswith ( " # " ) :
channel = name
2024-12-15 22:07:30 +00:00
try :
message_body = line [ line . index ( " : " ) + 2 : ]
except ( IndexError , ValueError ) :
message_body = " "
2024-12-15 21:32:00 +00:00
if message_body :
for callback in self . searchers :
2024-12-16 03:36:08 +00:00
result = callback ( message_body )
if result :
self . send ( channel , result )
for command , callback in self . commands :
if message_body . lower ( ) . startswith ( command ) :
try :
arguments = line [ line . index ( command ) + len ( command ) + 1 : ]
except ( IndexError , ValueError ) :
arguments = None
result = callback ( channel , name , arguments )
if result :
self . send ( channel , result )
2024-12-15 05:49:47 +00:00
def run ( ) :
bot = IRCBot (
nick ,
realname ,
helptext ,
2024-12-15 07:00:05 +00:00
[ # endswith commands
2024-12-15 21:06:47 +00:00
( " !help " , post_help ) ,
2024-12-15 21:32:00 +00:00
( " !rollcall " , post_help ) ,
2024-12-16 03:36:08 +00:00
( " !birds " , post_birds ) ,
2024-12-15 05:49:47 +00:00
( " !trivia " , post_question ) ,
( " !aitrivia " , post_ai_question ) ,
( " !trscores " , post_top_scores ) ,
( " !aiscores " , post_top_ai_scores ) ,
( " true " , answer_true ) ,
( " false " , answer_false ) ,
2024-12-15 19:17:47 +00:00
( " right " , answer_right ) ,
( " wrong " , answer_wrong )
2024-12-15 05:49:47 +00:00
] ,
2024-12-15 07:00:05 +00:00
[ # message searchers
# empty
] ,
2024-12-15 05:49:47 +00:00
channels
)
while True :
2024-12-15 21:32:00 +00:00
sleep ( 0.5 )
2024-12-15 22:07:30 +00:00
bot . command_loop ( )
2024-12-15 05:49:47 +00:00
2024-12-16 03:36:08 +00:00
# if __name__ == "__main__":
# run()