Compare commits

...

8 Commits

Author SHA1 Message Date
Tanner 348c010b7c Ignore aider 2026-06-13 16:34:36 -06:00
tanner 8b65026401 Add drugwars game 2026-02-18 03:54:21 +00:00
tanner 52c417b176 Add Protovac quotes 2025-11-19 02:26:05 +00:00
tanner 6ff226543d Unhide forum label menu option 2025-08-03 16:42:36 -06:00
tanner fe299ba2b5 Finish forum label generation 2025-08-03 16:42:00 -06:00
tanner 7bc5e02a7c Hide forum thread label option for now 2025-08-03 16:11:25 -06:00
tanner 016d40938c Adjust train speeds 2025-08-02 19:37:58 -06:00
tanner c0fe15cfb8 UI for printing forum thread labels 2025-08-02 19:37:58 -06:00
4 changed files with 346 additions and 19 deletions
+1
View File
@@ -115,3 +115,4 @@ venv.bak/
secrets.py secrets.py
tmp.png tmp.png
.aider*
+92
View File
@@ -0,0 +1,92 @@
from PIL import Image, ImageEnhance, ImageFont, ImageDraw
import os
import textwrap
import qrcode
import urllib.parse
location = os.path.dirname(os.path.realpath(__file__))
def print_forum_label(thread):
im = Image.open(location + '/label.png')
width, height = im.size
draw = ImageDraw.Draw(im)
#logging.info('Printing forum thread: %s', 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
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')
print_forum_label(dict(id=10197, title='Pitch: A wild split-flap display appeared!'))
+247 -10
View File
@@ -26,6 +26,7 @@ import textwrap
import random import random
import qrcode import qrcode
import urllib.parse import urllib.parse
import unicodedata
from PIL import Image, ImageEnhance, ImageFont, ImageDraw from PIL import Image, ImageEnhance, ImageFont, ImageDraw
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import paho.mqtt.publish as publish import paho.mqtt.publish as publish
@@ -48,6 +49,7 @@ KEY_SPACE = 32
TIMEZONE_CALGARY = pytz.timezone('America/Edmonton') TIMEZONE_CALGARY = pytz.timezone('America/Edmonton')
DRUGWARS_LOCATION = '/home/pi/protovac/env/bin/drugwars'
NETHACK_LOCATION = '/usr/games/nethack' NETHACK_LOCATION = '/usr/games/nethack'
MORIA_LOCATION = '/usr/games/moria' MORIA_LOCATION = '/usr/games/moria'
_2048_LOCATION = '/home/pi/2048-cli/2048' _2048_LOCATION = '/home/pi/2048-cli/2048'
@@ -55,6 +57,7 @@ FROTZ_LOCATION = '/usr/games/frotz'
HITCHHIKERS_LOCATION = '/home/pi/frotz/hhgg.z3' HITCHHIKERS_LOCATION = '/home/pi/frotz/hhgg.z3'
SUDOKU_LOCATION = '/usr/games/nudoku' SUDOKU_LOCATION = '/usr/games/nudoku'
HAS_DRUGWARS = os.path.isfile(DRUGWARS_LOCATION)
HAS_NETHACK = os.path.isfile(NETHACK_LOCATION) HAS_NETHACK = os.path.isfile(NETHACK_LOCATION)
HAS_MORIA = os.path.isfile(MORIA_LOCATION) HAS_MORIA = os.path.isfile(MORIA_LOCATION)
HAS_2048 = os.path.isfile(_2048_LOCATION) HAS_2048 = os.path.isfile(_2048_LOCATION)
@@ -86,6 +89,12 @@ def format_date(datestr):
d = d.astimezone(TIMEZONE_CALGARY) d = d.astimezone(TIMEZONE_CALGARY)
return d.strftime('%a %b %-d, %Y %-I:%M %p') 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): def sign_send(to_send):
try: try:
logging.info('Sending to sign: %s', to_send) logging.info('Sending to sign: %s', to_send)
@@ -249,6 +258,39 @@ QUOTES = [
'ACID BURN', 'ACID BURN',
'CEREAL KILLER', 'CEREAL KILLER',
'ZERO COOL', '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) random.shuffle(QUOTES)
@@ -458,6 +500,103 @@ def print_consumable_label(item):
os.system('lp -d dymo tmp.png > /dev/null 2>&1') 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
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):
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:
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': secrets.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): def message_protovac(thread):
try: try:
logging.info('Message to Protovac: %s', thread[-1]['content']) logging.info('Message to Protovac: %s', thread[-1]['content'])
@@ -599,6 +738,8 @@ label_material_name = ''
label_material_contact = '' label_material_contact = ''
label_generic = '' label_generic = ''
label_consumable = '' label_consumable = ''
label_forum_search = ''
search_results = None
logging.info('Starting main loop...') logging.info('Starting main loop...')
@@ -607,7 +748,7 @@ last_key = time.time()
def ratelimit_key(): def ratelimit_key():
global last_key global last_key
if think_to_send or sign_to_send or message_to_send or nametag_member or nametag_guest or label_tool or label_material_name or label_material_contact or label_generic or label_consumable or time.time() > last_key + 1: if think_to_send or sign_to_send or message_to_send or nametag_member or nametag_guest or label_tool or label_material_name or label_material_contact or label_generic or label_consumable or label_forum_search or time.time() > last_key + 1:
last_key = time.time() last_key = time.time()
return False return False
else: else:
@@ -925,7 +1066,11 @@ while True:
stdscr.addstr(0, 1, 'PROTOVAC UNIVERSAL COMPUTER') stdscr.addstr(0, 1, 'PROTOVAC UNIVERSAL COMPUTER')
stdscr.addstr(2, 1, 'Print a Label') stdscr.addstr(2, 1, 'Print a Label')
stdscr.addstr(3, 1, '===============') stdscr.addstr(3, 1, '===============')
stdscr.addstr(5, 1, 'Choose the type of label.') if search_results is None:
stdscr.addstr(5, 1, 'Choose the type of label.')
else:
stdscr.addstr(5, 1, 'Choose the thread to print:')
stdscr.clrtoeol()
if label_tool: if label_tool:
stdscr.addstr(8, 4, 'Enter Wiki-ID tool number: ' + label_tool) stdscr.addstr(8, 4, 'Enter Wiki-ID tool number: ' + label_tool)
@@ -934,6 +1079,8 @@ while True:
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(12, 4, '') stdscr.addstr(12, 4, '')
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(14, 4, '')
stdscr.clrtoeol()
stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel') stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel')
elif label_material_contact: elif label_material_contact:
stdscr.addstr(8, 4, '') stdscr.addstr(8, 4, '')
@@ -942,6 +1089,8 @@ while True:
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(12, 4, '') stdscr.addstr(12, 4, '')
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(14, 4, '')
stdscr.clrtoeol()
stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel') stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel')
elif label_material_name: elif label_material_name:
stdscr.addstr(8, 4, '') stdscr.addstr(8, 4, '')
@@ -950,6 +1099,8 @@ while True:
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(12, 4, '') stdscr.addstr(12, 4, '')
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(14, 4, '')
stdscr.clrtoeol()
stdscr.addstr(23, 1, '[RETURN] Next [ESC] Cancel') stdscr.addstr(23, 1, '[RETURN] Next [ESC] Cancel')
elif label_generic: elif label_generic:
stdscr.addstr(8, 4, '') stdscr.addstr(8, 4, '')
@@ -958,6 +1109,8 @@ while True:
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(12, 4, 'Enter your message: ' + label_generic) stdscr.addstr(12, 4, 'Enter your message: ' + label_generic)
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(14, 4, '')
stdscr.clrtoeol()
stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel') stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel')
elif label_consumable: elif label_consumable:
stdscr.addstr(8, 4, '') stdscr.addstr(8, 4, '')
@@ -966,11 +1119,36 @@ while True:
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(12, 4, 'Enter the item: ' + label_consumable) stdscr.addstr(12, 4, 'Enter the item: ' + label_consumable)
stdscr.clrtoeol() stdscr.clrtoeol()
stdscr.addstr(14, 4, '')
stdscr.clrtoeol()
stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel') stdscr.addstr(23, 1, '[RETURN] Print [ESC] Cancel')
elif label_forum_search:
stdscr.addstr(8, 4, '')
stdscr.clrtoeol()
stdscr.addstr(10, 4, '')
stdscr.clrtoeol()
stdscr.addstr(12, 4, '')
stdscr.clrtoeol()
stdscr.addstr(14, 4, 'Search for a thread: ' + label_forum_search)
stdscr.clrtoeol()
stdscr.addstr(23, 1, '[RETURN] Search [ESC] Cancel')
elif search_results is not None:
if len(search_results):
for i, result in enumerate(search_results):
result_title = truncate_string(result['title'], 74)
stdscr.addstr(7 + i*2, 1, '[{}]'.format(i+1), curses.A_REVERSE if highlight_keys else 0)
stdscr.addstr(7 + i*2, 5, ' {}'.format(result_title))
stdscr.clrtoeol()
stdscr.addstr(23, 1, '[ESC] Cancel')
else:
stdscr.addstr(8, 4, 'No results, try again.')
stdscr.addstr(23, 1, '[ESC] Cancel', curses.A_REVERSE if highlight_keys else 0)
else: else:
stdscr.addstr(8, 4, '[T] Tool label', curses.A_REVERSE if highlight_keys else 0) stdscr.addstr(8, 4, '[T] Tool label', curses.A_REVERSE if highlight_keys else 0)
stdscr.addstr(10, 4, '[S] Sheet material', curses.A_REVERSE if highlight_keys else 0) stdscr.addstr(10, 4, '[S] Sheet material', curses.A_REVERSE if highlight_keys else 0)
stdscr.addstr(12, 4, '[G] Generic label', curses.A_REVERSE if highlight_keys else 0) stdscr.addstr(12, 4, '[G] Generic label', curses.A_REVERSE if highlight_keys else 0)
stdscr.addstr(14, 4, '[F] Forum thread', curses.A_REVERSE if highlight_keys else 0)
stdscr.addstr(23, 1, '[B] Back', curses.A_REVERSE if highlight_keys else 0) stdscr.addstr(23, 1, '[B] Back', curses.A_REVERSE if highlight_keys else 0)
stdscr.clrtoeol() stdscr.clrtoeol()
@@ -992,6 +1170,8 @@ while True:
stdscr.addstr(14, 4, '[H] Hitchhiker\'s Guide to the Galaxy', curses.A_REVERSE if highlight_keys else 0) stdscr.addstr(14, 4, '[H] Hitchhiker\'s Guide to the Galaxy', curses.A_REVERSE if highlight_keys else 0)
if HAS_SUDOKU: if HAS_SUDOKU:
stdscr.addstr(16, 4, '[S] Sudoku', curses.A_REVERSE if highlight_keys else 0) stdscr.addstr(16, 4, '[S] Sudoku', curses.A_REVERSE if highlight_keys else 0)
if HAS_DRUGWARS:
stdscr.addstr(18, 4, '[D] Drugwars', curses.A_REVERSE if highlight_keys else 0)
stdscr.addstr(23, 1, '[B] Back', curses.A_REVERSE if highlight_keys else 0) stdscr.addstr(23, 1, '[B] Back', curses.A_REVERSE if highlight_keys else 0)
@@ -1110,7 +1290,7 @@ while True:
button = None button = None
def try_highlight(): def try_highlight():
global c, highlight_debounce, highlight_keys, highlight_count, current_screen global c, highlight_debounce, highlight_keys, highlight_count, current_screen, search_results
if c and time.time() - highlight_debounce > 0.6: if c and time.time() - highlight_debounce > 0.6:
highlight_debounce = time.time() highlight_debounce = time.time()
@@ -1122,6 +1302,7 @@ while True:
if highlight_count >= 3: if highlight_count >= 3:
highlight_count = 0 highlight_count = 0
current_screen = 'help' current_screen = 'help'
search_results = None
if current_screen == 'home': if current_screen == 'home':
if button == 's': if button == 's':
@@ -1428,6 +1609,52 @@ I will be terse in my responses.
else: else:
if c < 127 and c > 31: if c < 127 and c > 31:
label_consumable = label_consumable[:-1] + chr(c) + '_' label_consumable = label_consumable[:-1] + chr(c) + '_'
elif label_forum_search:
if c == curses.KEY_BACKSPACE:
label_forum_search = label_forum_search[:-2] + '_'
elif c == KEY_ESCAPE:
label_forum_search = ''
stdscr.erase()
elif c == KEY_ENTER:
if len(label_forum_search) > 2:
stdscr.addstr(16, 4, 'Searching...')
stdscr.refresh()
try:
search_results = search_forum_thread(label_forum_search[:-1])
except BaseException as e:
logging.exception(e)
stdscr.addstr(16, 4, 'Error.')
stdscr.clrtoeol()
stdscr.refresh()
time.sleep(2)
stdscr.erase()
label_forum_search = ''
else:
if c < 127 and c > 31:
label_forum_search = label_forum_search[:-1] + chr(c) + '_'
elif search_results is not None:
if c == KEY_ESCAPE or button == 'b':
search_results = None
stdscr.erase()
elif c >= 49 and c <= 57:
num = int(chr(c))
if num <= len(search_results):
stdscr.addstr(21, 1, 'Printing...')
stdscr.refresh()
try:
print_forum_label(search_results[num-1])
except BaseException as e:
logging.exception(e)
stdscr.addstr(15, 4, 'Error.')
stdscr.clrtoeol()
stdscr.refresh()
time.sleep(2)
stdscr.erase()
label_consumable = ''
else:
try_highlight()
else:
try_highlight()
elif button == 'b' or c == KEY_ESCAPE: elif button == 'b' or c == KEY_ESCAPE:
current_screen = 'home' current_screen = 'home'
elif button == 't': elif button == 't':
@@ -1438,6 +1665,8 @@ I will be terse in my responses.
label_generic = '_' label_generic = '_'
elif button == 'c': elif button == 'c':
label_consumable = '_' label_consumable = '_'
elif button == 'f':
label_forum_search = '_'
else: else:
try_highlight() try_highlight()
@@ -1465,7 +1694,7 @@ I will be terse in my responses.
stdscr.keypad(False) stdscr.keypad(False)
curses.echo() curses.echo()
curses.endwin() curses.endwin()
logging.info('Spawning moria.') logging.info('Spawning 2048.')
os.system(_2048_LOCATION) os.system(_2048_LOCATION)
break break
elif button == 'm' and HAS_MORIA: elif button == 'm' and HAS_MORIA:
@@ -1484,6 +1713,14 @@ I will be terse in my responses.
logging.info('Spawning nethack.') logging.info('Spawning nethack.')
os.system(NETHACK_LOCATION) os.system(NETHACK_LOCATION)
break break
elif button == 'd' and HAS_DRUGWARS:
curses.nocbreak()
stdscr.keypad(False)
curses.echo()
curses.endwin()
logging.info('Spawning drugwars.')
os.system(DRUGWARS_LOCATION)
break
else: else:
try_highlight() try_highlight()
@@ -1551,14 +1788,14 @@ I will be terse in my responses.
res = '' res = ''
if button == 'r': if button == 'r':
res = mqtt_publish('train/control/speed', 50) res = mqtt_publish('train/control/speed', -300)
logging.info('Setting train speed to: 50') logging.info('Setting train speed to: -300')
elif button == 't' or c == KEY_SPACE: elif button == 't' or c == KEY_SPACE:
res = mqtt_publish('train/control/speed', 90) res = mqtt_publish('train/control/speed', 0)
logging.info('Setting train speed to: 90') logging.info('Setting train speed to: 0')
elif button == 'f': elif button == 'f':
res = mqtt_publish('train/control/speed', 140) res = mqtt_publish('train/control/speed', 300)
logging.info('Setting train speed to: 140') logging.info('Setting train speed to: 300')
elif button == 'b' or c == KEY_ESCAPE: elif button == 'b' or c == KEY_ESCAPE:
current_screen = 'home' current_screen = 'home'
+6 -9
View File
@@ -1,11 +1,8 @@
wa_api_key = '' wa_api_key = ''
# Get these from your network inspector after sending a message to character.ai openai_key = ''
character_ai_cookies = {
'_legacy_auth0.whatever.is.authenticated': 'true', MQTT_WRITER_PASSWORD = ''
'auth0.whatever.is.authenticated': 'true',
'messages': '', FORUM_SEARCH_API_KEY = ''
'csrftoken': '',
'sessionid': '',
}
character_ai_token = ''