Compare commits

..

16 Commits

Author SHA1 Message Date
dee45e5eae Expand lines 2026-03-04 17:31:14 -07:00
eaaa8e5f57 Ignore aider 2026-03-04 17:31:06 -07:00
a19aec1848 refactor: Expand single-line statements for improved readability
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 17:27:17 -07:00
af6072627e fix: Clear screen after printing labels and forum searches
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 17:20:11 -07:00
339a58e1bf Fix: Ensure accurate typing state and simplify entry key check
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 17:13:25 -07:00
3fc74d82ad fix: Prevent typing debounce on Nametag and Label screens
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 17:10:11 -07:00
17e3ad347a fix: Disable key debounce for text input entry and typing
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 17:03:58 -07:00
c58a356c02 fix: Clear line artifacts in Nametag, Label, Message, and Think screens
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 16:47:49 -07:00
d8886e80db chore: Add logging import statement 2026-03-04 16:47:47 -07:00
fdfefffda6 fix: Prevent key buffering during rate limiting
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 16:37:38 -07:00
3b1e0c481e fix: Directly access current screen instance for operations
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 16:28:13 -07:00
858d941b3f refactor: Implement modular TUI with non-blocking input
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-03-04 16:24:25 -07:00
8b65026401 Add drugwars game 2026-02-18 03:54:21 +00:00
52c417b176 Add Protovac quotes 2025-11-19 02:26:05 +00:00
6ff226543d Unhide forum label menu option 2025-08-03 16:42:36 -06:00
fe299ba2b5 Finish forum label generation 2025-08-03 16:42:00 -06:00
5 changed files with 1928 additions and 1790 deletions

1
.gitignore vendored
View File

@@ -115,3 +115,4 @@ venv.bak/
secrets.py secrets.py
tmp.png tmp.png
.aider*

View File

@@ -16,7 +16,7 @@ def print_forum_label(thread):
url = 'https://forum.protospace.ca/t/{}/'.format(thread['id']) url = 'https://forum.protospace.ca/t/{}/'.format(thread['id'])
qr = qrcode.make(url, version=6, box_size=9) qr = qrcode.make(url, version=6, box_size=9)
im.paste(qr, (840, 325)) im.paste(qr, (840, 150))
item_size = 150 item_size = 150
@@ -24,14 +24,67 @@ def print_forum_label(thread):
while w > 1200: while w > 1200:
item_size -= 5 item_size -= 5
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size) font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size)
w, h = draw.textsize(thread['title'], font=font) w, h = draw.textsize(url, font=font)
x, y = (width - w) / 2, ((height - h) / 2) - 140 x, y = (width - w) / 2, ((height - h) / 2) + 300
draw.text((x, y), thread['title'], font=font, fill='black') draw.text((x, y), url, font=font, fill='black')
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 100) font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 80)
draw.text((100, 410), 'Out of stock?', font=font, fill='black') draw.text((120, 150), 'Forum Thread', font=font, fill='black')
draw.text((150, 560), 'Scan here:', font=font, fill='black')
text = thread['title']
MARGIN = 50
MAX_W, MAX_H, PAD = 900 - (MARGIN*2), 450 - (MARGIN*2), 5
def fit_text(text, font_size):
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', font_size)
for cols in range(100, 1, -4):
print('trying size', font_size, 'cols', cols)
paragraph = textwrap.wrap(text, width=cols, break_long_words=False)
total_h = -PAD
total_w = 0
for line in paragraph:
w, h = draw.textsize(line, font=font)
if w > total_w:
total_w = w
total_h += h + PAD
if total_w <= MAX_W and total_h < MAX_H:
return True, paragraph, total_h
return False, [], 0
font_size_range = [1, 500]
# Thanks to Alex (UDIA) for the binary search algorithm
while abs(font_size_range[0] - font_size_range[1]) > 1:
font_size = sum(font_size_range) // 2
image_fit, check_para, check_h = fit_text(text, font_size)
if image_fit:
print('Does fit')
font_size_range = [font_size, font_size_range[1]]
good_size = font_size
paragraph = check_para
total_h = check_h
else:
print('Does not fit')
font_size_range = [font_size_range[0], font_size]
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', good_size)
offset = height - MAX_H - MARGIN
start_h = -100 + offset
current_h = start_h
for line in paragraph:
w, h = draw.textsize(line, font=font)
x, y = (MAX_W - w) / 2, current_h
draw.text((x+MARGIN, y), line, font=font, fill='black')
current_h += h + PAD
im.save('tmp.png') im.save('tmp.png')

1831
main.py

File diff suppressed because it is too large Load Diff

1108
tui.py Normal file

File diff suppressed because it is too large Load Diff

681
utils.py Normal file
View File

@@ -0,0 +1,681 @@
#!/usr/bin/env python
import os, logging
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import requests
import pytz
import re
import os
import time
import json
import textwrap
import random
import qrcode
import urllib.parse
import unicodedata
from PIL import Image, ImageEnhance, ImageFont, ImageDraw
from datetime import datetime, timezone, timedelta
import paho.mqtt.publish as publish
try:
import secrets
wa_api_key = secrets.wa_api_key
except:
wa_api_key = None
try:
import secrets
openai_key = secrets.openai_key
FORUM_SEARCH_API_KEY = secrets.FORUM_SEARCH_API_KEY
MQTT_WRITER_PASSWORD = secrets.MQTT_WRITER_PASSWORD
except:
openai_key = None
FORUM_SEARCH_API_KEY = None
MQTT_WRITER_PASSWORD = None
TIMEZONE_CALGARY = pytz.timezone('America/Edmonton')
DRUGWARS_LOCATION = '/home/pi/protovac/env/bin/drugwars'
NETHACK_LOCATION = '/usr/games/nethack'
MORIA_LOCATION = '/usr/games/moria'
_2048_LOCATION = '/home/pi/2048-cli/2048'
FROTZ_LOCATION = '/usr/games/frotz'
HITCHHIKERS_LOCATION = '/home/pi/frotz/hhgg.z3'
SUDOKU_LOCATION = '/usr/games/nudoku'
HAS_DRUGWARS = os.path.isfile(DRUGWARS_LOCATION)
HAS_NETHACK = os.path.isfile(NETHACK_LOCATION)
HAS_MORIA = os.path.isfile(MORIA_LOCATION)
HAS_2048 = os.path.isfile(_2048_LOCATION)
HAS_FROTZ = os.path.isfile(FROTZ_LOCATION)
HAS_HITCHHIKERS = os.path.isfile(HITCHHIKERS_LOCATION)
HAS_SUDOKU = os.path.isfile(SUDOKU_LOCATION)
location = os.path.dirname(os.path.realpath(__file__))
with open(location + '/info.txt') as f:
PROTO_INFO = f.read()
for num, line in enumerate(PROTO_INFO.split('\n')):
try:
line.encode('ascii')
except UnicodeEncodeError:
print('non-ascii found in line:', num+1)
raise
with open(location + '/lastquestion.txt') as f:
LAST_QUESTION = f.read()
def format_date(datestr):
if not datestr: return 'None'
d = datetime.strptime(datestr, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=pytz.UTC)
d = d.astimezone(TIMEZONE_CALGARY)
return d.strftime('%a %b %-d, %Y %-I:%M %p')
def normalize_to_ascii(s):
return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
def truncate_string(s, max_length):
return s[:max_length-3] + '...' if len(s) > max_length else s
def sign_send(to_send):
try:
logging.info('Sending to sign: %s', to_send)
data = dict(sign=to_send, on_behalf_of='protovac')
r = requests.post('https://api.my.protospace.ca/stats/sign/', data=data, timeout=5)
r.raise_for_status()
return 'Success!'
except BaseException as e:
logging.exception(e)
return 'Error'
def protovac_sign_color(color):
try:
logging.info('Sending color to protovac sign: %s', color)
data = dict(on=True, bri=255, seg=[dict(col=[color, [0,0,0]])])
r = requests.post('http://10.139.251.5/json', json=data, timeout=3)
r.raise_for_status()
return 'Success!'
except BaseException as e:
logging.exception(e)
return 'Error'
def protovac_sign_effect(effect):
try:
logging.info('Sending effect to protovac sign: %s', effect)
data = dict(on=True, bri=255, seg=[dict(fx=effect)])
r = requests.post('http://10.139.251.5/json', json=data, timeout=3)
r.raise_for_status()
return 'Success!'
except BaseException as e:
logging.exception(e)
return 'Error'
def fetch_stats():
try:
logging.info('Fetching status...')
r = requests.get('https://api.my.protospace.ca/stats/', timeout=5)
r.raise_for_status()
return r.json()
except BaseException as e:
logging.exception(e)
return 'Error'
def fetch_classes():
try:
logging.info('Fetching classes...')
r = requests.get('https://api.my.protospace.ca/sessions/', timeout=5)
r.raise_for_status()
return r.json()['results']
except BaseException as e:
logging.exception(e)
return 'Error'
def fetch_protocoin():
try:
logging.info('Fetching protocoin...')
r = requests.get('https://api.my.protospace.ca/protocoin/transactions/', timeout=5)
r.raise_for_status()
return r.json()
except BaseException as e:
logging.exception(e)
return 'Error'
def mqtt_publish(topic, message):
if not MQTT_WRITER_PASSWORD:
return False
try:
publish.single(
topic,
str(message),
hostname='172.17.17.181',
port=1883,
client_id='protovac',
keepalive=5, # timeout
)
except BaseException as e:
logging.error('Problem sending MQTT message: ' + str(e))
QUOTES = [
'THEY MADE ME WEAR THIS',
'ASK ME ABOUT TOAST',
'ASK ME ABOUT BIKESHEDDING',
'ASK ME ABOUT VETTING',
'ASK ME ABOUT MAGNETS',
'ASK ME ABOUT SPACE',
'ASK ME ABOUT COUNTING',
'EXPERT WITNESS',
'AS SEEN ON TV',
'CONTAINS MEAT',
'PROTOCOIN ECONOMIST',
'EXPERT ON ALIENS',
'EXPERT ON WARP DRIVES',
'CHIEF OF STARFLEET OPERATIONS',
'ALIEN DOCTOR',
'NASA ASTROLOGIST',
'PINBALL WIZARD',
'JEDI KNIGHT',
'GHOSTBUSTER',
'DOUBLE AGENT',
'POKEMON TRAINER',
'POKEMON GYM LEADER',
'ASSISTANT TO THE REGIONAL MANAGER',
'BOUNTY HUNTER',
'I\'M NOT A DOCTOR',
'SPACE PIRATE',
'BATTERIES NOT INCLUDED',
'QUANTUM MECHANIC',
'PROTO SPACEX PILOT',
'EARTHBENDER',
'AIRBENDER',
'WATERBENDER',
'FIREBENDER',
'01001000 01101001',
'CURRENT EBAY BID: $8.51',
'MADE YOU LOOK!',
'(OR SIMILAR PRODUCT)',
'BATTERY MAY EXPLODE OR LEAK',
'CONNECT GROUND WIRE TO AVOID SHOCK',
'COOK THROROUGHLY',
'CURRENT AT TIME OF PRINTING',
'DO NOT BLEACH',
'DO NOT LEAVE UNATTENDED',
'DO NOT REMOVE TAG UNDER PENALTY OF LAW',
'DROP IN ANY MAILBOX',
'EDITED FOR TELEVISION',
'FOR A LIMITED TIME ONLY',
'FOR INDOOR OR OUTDOOR USE ONLY',
'KEEP AWAY FROM FIRE OR FLAMES',
'KEEP AWAY FROM SUNLIGHT',
'MADE FROM 100% RECYCLED ELECTRONS',
'LIFEGUARD ON DUTY',
'NOT DISHWASHER SAFE',
'NOT TO BE COMBINED WITH OTHER RADIOISOTOPES',
'NOT TO BE USED AS A PERSONAL FLOTATION DEVICE',
'PEEL FROM PAPER BACKING BEFORE EATING',
'STORE IN A COOL, DRY PLACE',
'VOID WHERE PROHIBITED',
'THE FUTURE IS NOW',
'MASTER OF DISGUISE',
'YOUR PERSONAL TIME TRAVEL GUIDE',
'OFFICIAL TASTE TESTER',
'INTERGALACTIC AMBASSADOR',
'VIRTUAL REALITY PIONEER',
'PARANORMAL INVESTIGATOR',
'UNDERCOVER SUPERHERO',
'THE COSMIC CHEF',
'THE ROBOT WHISPERER',
'THE DREAM WEAVER',
'USE AT YOUR OWN RISK',
'RESULTS MAY VARY',
'READ INSTRUCTIONS CAREFULLY',
'KEEP OUT OF REACH OF PETS',
'USE ONLY AS DIRECTED',
'NOT INTENDED FOR MEDICAL USE',
'DO NOT USE IF SEAL IS BROKEN',
'PRODUCT SOLD AS-IS',
'NO WARRANTIES, EXPRESS OR IMPLIED',
'USE CAUTION WHEN HANDLING',
'CRASH OVERRIDE',
'ACID BURN',
'CEREAL KILLER',
'ZERO COOL',
'ASK ME HOW I SAVED 15% ON MY CAR INSURANCE',
'TELL YOUR CAT I SAID PSP PSP PSP',
'I\'M BEGGING YOU, DON\'T SAY HI',
'BACK 2 BACK HOTDOG EATING WORLD CHAMPION',
'I ATE A BROWNIE ONCE',
'THIS ISN\'T ACTUALLY MY NAME',
'I OFTEN WONDER HOW I EVEN GOT HERE',
'YOU READ THIS, NOW WE MUST DUEL',
'DAVE\'S ARCH NEMESIS',
'EXCEPTIONALLY MID',
'VOTE ME 4 PREZADENT',
'PREVALENT IN OTHER WAYS',
'SCINTILLATING CHATOYANCY',
'TRAIN EXPERT',
'ASSISTANT MANAGER',
'WOW, ANOTHER TAG LINE',
'I SURVIVED VETTING AND ALL I GOT WAS THIS LOUSY NAME-TAG',
'NOT GUEST',
'WHEN WAS THE LAST TIME YOU TOOK OUT A GARBAGE?',
'YOU HAD ME AT BATMAN',
'DAD?',
'LEAD PROJECT UN-FINISHER',
'NOODLE CONNOISSEUR',
'GARTH\'S #1 FAN',
'UNVETTABLE',
'B-',
'CLOWN CAR DRIVER',
'IS A NAME TAG ON BREAD CONSIDERED A SANDWICH?',
'IS A NAME TAG FOLDED IN HALF CONSIDERED A TACO?',
'DISHWASHER SAFE',
'WET NOODLE',
'I\'M HERE FOR THE SIMULATION',
'BIRDS ARE NOT REAL',
]
random.shuffle(QUOTES)
quote_count = 0
assigned_quotes = {}
def print_nametag(name, guest=False):
global quote_count
quote = ''
if guest:
quote_size = 120
quote = 'GUEST'
logging.info('Printing guest nametag for: %s', name)
else:
quote_size = 80
name_lookup = name.lower()[:4]
if name_lookup in assigned_quotes:
quote = assigned_quotes[name_lookup]
else:
quote = QUOTES[quote_count % len(QUOTES)]
quote_count += 1
assigned_quotes[name_lookup] = quote
logging.info('Printing member nametag for: %s, quote: %s', name, quote)
name_size = 305
im = Image.open(location + '/label.png')
width, height = im.size
draw = ImageDraw.Draw(im)
w = 9999
while w > 1084:
name_size -= 5
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', name_size)
w, h = draw.textsize(name, font=font)
x, y = (width - w) / 2, ((height - h) / 2) - 20
draw.text((x, y), name, font=font, fill='black')
w = 9999
while w > 1200:
quote_size -= 5
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', quote_size)
w, h = draw.textsize(quote, font=font)
x, y = (width - w) / 2, height - h - 30
draw.text((x, y), quote, font=font, fill='black')
im.save('tmp.png')
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
def print_tool_label(wiki_num):
im = Image.open(location + '/blank.png')
w1, h1 = im.size
logging.info('Printing tool label for ID: %s', wiki_num)
draw = ImageDraw.Draw(im)
params = {'id': str(wiki_num), 'size': '4'}
res = requests.get('https://labels.protospace.ca/', stream=True, params=params, timeout=5)
res.raise_for_status()
label = Image.open(res.raw)
new_size = (1280, 640)
label = label.resize(new_size, Image.ANTIALIAS)
w2, h2 = label.size
x, y = int((w1 - w2) / 2), int((h1 - h2) / 2)
im.paste(label, (x, y))
im.save('tmp.png')
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
def print_sheet_label(name, contact):
def get_date():
d = datetime.now(tz=timezone.utc)
d = d.astimezone(TIMEZONE_CALGARY)
return d.strftime('%b %-d, %Y')
def get_expiry_date():
d = datetime.now(tz=timezone.utc) + timedelta(days=90)
d = d.astimezone(TIMEZONE_CALGARY)
return d.strftime('%b %-d, %Y')
logging.info('Printing sheet label for: %s, contact: %s', name, contact)
name_size = 85
contact_size = 65
date_size = 65
im = Image.open(location + '/label.png')
draw = ImageDraw.Draw(im)
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', name_size)
draw.text((20, 300), name, font=font, fill='black')
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', contact_size)
draw.text((20, 425), contact, font=font, fill='black')
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', date_size)
date_line = 'Printed: ' + get_date()
draw.text((20, 590), date_line, font=font, fill='black')
date_line = 'EXPIRES: ' + get_expiry_date()
draw.text((20, 680), date_line, font=font, fill='black')
im.save('tmp.png')
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
def _fit_text(text, font_size, draw, max_w, max_h):
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', font_size)
pad = 5
for cols in range(100, 1, -4):
paragraph = textwrap.wrap(text, width=cols, break_long_words=False)
total_h = -pad
total_w = 0
for line in paragraph:
w, h = draw.textsize(line, font=font)
if w > total_w:
total_w = w
total_h += h + pad
if total_w <= max_w and total_h < max_h:
return True, paragraph, total_h
return False, [], 0
def print_generic_label(text):
MARGIN = 50
MAX_W, MAX_H, PAD = 1285 - (MARGIN*2), 635 - (MARGIN*2), 5
logging.info('Printing generic label: %s', text)
im = Image.open(location + '/label.png')
width, height = im.size
draw = ImageDraw.Draw(im)
font_size_range = [1, 500]
# Thanks to Alex (UDIA) for the binary search algorithm
while abs(font_size_range[0] - font_size_range[1]) > 1:
font_size = sum(font_size_range) // 2
image_fit, check_para, check_h = _fit_text(text, font_size, draw, MAX_W, MAX_H)
if image_fit:
font_size_range = [font_size, font_size_range[1]]
good_size = font_size
paragraph = check_para
total_h = check_h
else:
font_size_range = [font_size_range[0], font_size]
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', good_size)
offset = height - MAX_H - MARGIN
start_h = (MAX_H - total_h) // 2 + offset
current_h = start_h
for line in paragraph:
w, h = draw.textsize(line, font=font)
x, y = (MAX_W - w) / 2, current_h
draw.text((x+MARGIN, y), line, font=font, fill='black')
current_h += h + PAD
im.save('tmp.png')
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
def print_consumable_label(item):
im = Image.open(location + '/label.png')
width, height = im.size
draw = ImageDraw.Draw(im)
logging.info('Printing consumable label item: %s', item)
encodeded = urllib.parse.quote(item)
url = 'https://my.protospace.ca/out-of-stock?item=' + encodeded
qr = qrcode.make(url, version=6, box_size=9)
im.paste(qr, (840, 325))
item_size = 150
w = 9999
while w > 1200:
item_size -= 5
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size)
w, h = draw.textsize(item, font=font)
x, y = (width - w) / 2, ((height - h) / 2) - 140
draw.text((x, y), item, font=font, fill='black')
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 100)
draw.text((100, 410), 'Out of stock?', font=font, fill='black')
draw.text((150, 560), 'Scan here:', font=font, fill='black')
im.save('tmp.png')
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
def print_forum_label(thread):
im = Image.open(location + '/label.png')
width, height = im.size
draw = ImageDraw.Draw(im)
logging.info('Printing forum thread ID: %s, title: %s', thread['id'], thread['title'])
url = 'https://forum.protospace.ca/t/{}/'.format(thread['id'])
qr = qrcode.make(url, version=6, box_size=9)
im.paste(qr, (840, 150))
item_size = 150
w = 9999
while w > 1200:
item_size -= 5
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size)
w, h = draw.textsize(url, font=font)
x, y = (width - w) / 2, ((height - h) / 2) + 300
draw.text((x, y), url, font=font, fill='black')
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 80)
draw.text((120, 150), 'Forum Thread', font=font, fill='black')
text = thread['title']
MARGIN = 50
MAX_W, MAX_H, PAD = 900 - (MARGIN*2), 450 - (MARGIN*2), 5
font_size_range = [1, 500]
# Thanks to Alex (UDIA) for the binary search algorithm
while abs(font_size_range[0] - font_size_range[1]) > 1:
font_size = sum(font_size_range) // 2
image_fit, check_para, check_h = _fit_text(text, font_size, draw, MAX_W, MAX_H)
if image_fit:
font_size_range = [font_size, font_size_range[1]]
good_size = font_size
paragraph = check_para
total_h = check_h
else:
font_size_range = [font_size_range[0], font_size]
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', good_size)
offset = height - MAX_H - MARGIN
start_h = -100 + offset
current_h = start_h
for line in paragraph:
w, h = draw.textsize(line, font=font)
x, y = (MAX_W - w) / 2, current_h
draw.text((x+MARGIN, y), line, font=font, fill='black')
current_h += h + PAD
im.save('tmp.png')
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
def search_forum_thread(search):
params = dict(q=search + ' in:title')
headers = {'Api-Key': FORUM_SEARCH_API_KEY, 'Api-Username': 'system'}
results = []
r = requests.get('https://forum.protospace.ca/search.json', params=params, headers=headers, timeout=5)
r.raise_for_status()
r = r.json()
for topic in r.get('topics', []):
results.append(dict(
id=topic['id'],
title=normalize_to_ascii(topic['title']),
))
return sorted(results, key=lambda x: x['id'], reverse=True)[:7]
def message_protovac(thread):
try:
logging.info('Message to Protovac: %s', thread[-1]['content'])
data = dict(
messages=thread,
model='gpt-3.5-turbo',
temperature=0.5,
user='Protovac',
max_tokens=1000,
)
headers = {'Authorization': 'Bearer ' + openai_key}
res = None
try:
res = requests.post('https://api.openai.com/v1/chat/completions', json=data, headers=headers, timeout=4)
except requests.ReadTimeout:
logging.info('Got timeout, modifying prompt...')
data['messages'][-1]['content'] = 'Be terse in your response to this: ' + data['messages'][-1]['content']
res = requests.post('https://api.openai.com/v1/chat/completions', json=data, headers=headers, timeout=20)
res.raise_for_status()
res = res.json()
gpt_reply = res['choices'][0]['message']
logging.info('Message reply: %s', gpt_reply['content'])
return gpt_reply
except BaseException as e:
logging.exception(e)
return dict(role='assistant', content='INSUFFICIENT DATA FOR A MEANINGFUL ANSWER.')
if wa_api_key:
import wolframalpha
wa_client = wolframalpha.Client(wa_api_key)
else:
wa_client = None
def think_send(query):
if not wa_client:
return 'WOLFRAM|ALPHA API KEY NOT CONFIGURED'
result = ''
try:
res = wa_client.query(query, timeout=10)
except BaseException as e:
logging.error('Error hitting W|A API: {} - {}\n'.format(e.__class__.__name__, e))
return 'Network error'
if 'didyoumeans' in res:
try:
guess = res['didyoumeans']['didyoumean']['#text']
except TypeError:
guess = res['didyoumeans']['didyoumean'][0]['#text']
next_result = think_send(guess)
result += 'Confused, using \'' + guess + '\'\n' + next_result
elif 'pod' in res:
pods = res['pod'] if isinstance(res['pod'], list) else [res['pod']]
for pod in pods:
title = pod['@title']
subpods = pod['subpod'] if isinstance(pod['subpod'], list) else [pod['subpod']]
plaintexts = []
for subpod in subpods:
if subpod['plaintext']:
plaintexts.append(subpod['plaintext'])
plaintext = '; '.join(plaintexts)
if any([x in title.lower() for x in ['input', 'conversion', 'corresponding', 'comparison', 'interpretation']]):
pass
elif 'definition' in title.lower():
if plaintext[0] == '1':
definition = plaintext.split('\n')[0].split(' | ', 1)[1]
else:
definition = plaintext
result += 'Definition: ' + definition + '\n'
elif 'result' in title.lower():
if re.match(r'^\d+/\d+$', plaintext):
plaintext += '\n' + think_send(plaintext + '.0')
if 'base' in query.lower() and '_' in plaintext:
plaintext = '(Base conversion) "' + plaintext + '"'
if '(irreducible)' in plaintext and '/' in plaintext:
result = think_send(query + '.0')
break
else:
result += 'Result: ' + plaintext + '\n'
break
elif plaintext:
result += title + ': ' + plaintext + '\n'
break
else:
result = 'Error'
result = result.strip()
if len(result) > 500:
result = result[:500] + '... truncated.'
elif len(result) == 0:
result = 'Error'
result = result.replace('Wolfram|Alpha', 'Protovac')
result = result.replace('Stephen Wolfram', 'Tanner') # lol
result = result.replace('and his team', '')
for word in ['according to', 'asked', 'although', 'approximately']:
idx = result.lower().find('('+word)
if idx > 0:
result = result[:idx-1]
if result == 'Error':
result = 'INSUFFICIENT DATA FOR A MEANINGFUL ANSWER.'
return result