2023-04-30 22:13:02 +00:00
|
|
|
|
2023-05-01 00:46:08 +00:00
|
|
|
import os
|
2023-04-30 22:13:02 +00:00
|
|
|
import json
|
|
|
|
import logging
|
2023-05-01 01:58:02 +00:00
|
|
|
import getpass
|
2023-05-01 04:34:56 +00:00
|
|
|
import textwrap
|
2023-05-01 05:55:20 +00:00
|
|
|
import threading
|
2023-05-01 14:35:31 +00:00
|
|
|
import re
|
2023-05-01 05:55:20 +00:00
|
|
|
import toml
|
2023-05-01 04:34:56 +00:00
|
|
|
import readchar
|
2023-05-01 05:55:20 +00:00
|
|
|
import websocket
|
2023-05-01 15:21:45 +00:00
|
|
|
import win_unicode_console
|
2023-05-01 04:34:56 +00:00
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
from collections import OrderedDict
|
2023-05-01 04:34:56 +00:00
|
|
|
from colorama import Fore, Back, Style, init as colorama_init
|
2023-05-01 01:58:02 +00:00
|
|
|
|
|
|
|
import contentapi
|
2023-05-01 01:03:03 +00:00
|
|
|
import myutils
|
2023-05-01 00:46:08 +00:00
|
|
|
|
|
|
|
CONFIGFILE="config.toml"
|
2023-05-01 15:21:45 +00:00
|
|
|
MAXTITLE=25
|
2023-04-30 22:13:02 +00:00
|
|
|
|
|
|
|
# The entire config object with all defaults
|
|
|
|
config = {
|
2023-04-30 22:49:30 +00:00
|
|
|
"api" : "https://oboy.smilebasicsource.com/api",
|
2023-05-01 01:12:14 +00:00
|
|
|
"default_loglevel" : "WARNING",
|
2023-05-01 05:55:20 +00:00
|
|
|
"websocket_trace" : False,
|
2023-05-01 04:34:56 +00:00
|
|
|
"default_room" : 0, # Zero means it will ask you for a room
|
2023-05-01 01:03:03 +00:00
|
|
|
"expire_seconds" : 31536000, # 365 days in seconds, expiration for token
|
2023-05-01 04:34:56 +00:00
|
|
|
"appear_in_global" : False,
|
2023-04-30 22:49:30 +00:00
|
|
|
"tokenfile" : ".qcstoken"
|
2023-04-30 22:13:02 +00:00
|
|
|
}
|
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
commands = OrderedDict([
|
|
|
|
("h" , "Help, prints this menu!"),
|
|
|
|
("s" , "Search, find and/or set a room to listen to (one at a time!)"),
|
|
|
|
("g" , "Global userlist, print users using contentapi in general"),
|
|
|
|
("u" , "Userlist, print users in the current room"),
|
|
|
|
("i" , "Insert mode, allows you to send a message (pauses messages!)"),
|
|
|
|
("q" , "Quit, no warning!")
|
|
|
|
])
|
|
|
|
|
2023-05-01 04:34:56 +00:00
|
|
|
|
2023-04-30 22:13:02 +00:00
|
|
|
def main():
|
2023-05-01 05:55:20 +00:00
|
|
|
|
2023-04-30 22:13:02 +00:00
|
|
|
print("Program start")
|
2023-05-01 15:21:45 +00:00
|
|
|
win_unicode_console.enable()
|
2023-05-01 04:34:56 +00:00
|
|
|
colorama_init() # colorama init
|
2023-05-01 05:55:20 +00:00
|
|
|
|
2023-05-01 00:46:08 +00:00
|
|
|
load_or_create_global_config()
|
2023-05-01 01:12:14 +00:00
|
|
|
logging.info("Config: " + json.dumps(config, indent = 2))
|
2023-05-01 05:55:20 +00:00
|
|
|
|
2023-04-30 22:57:50 +00:00
|
|
|
context = contentapi.ApiContext(config["api"], logging)
|
2023-05-01 01:58:02 +00:00
|
|
|
logging.info("Testing connection to API at " + config["api"])
|
|
|
|
logging.debug(json.dumps(context.api_status(), indent = 2))
|
|
|
|
authenticate(config, context)
|
2023-05-01 04:34:56 +00:00
|
|
|
|
|
|
|
# - Enter input loop, but check room number on "input" mode, don't let messages send in room 0
|
|
|
|
# - h to help
|
|
|
|
# - s to search rooms, enter #1234 to connect directly, empty string to quit
|
|
|
|
# - g to list global users
|
|
|
|
# - u to list users in room
|
|
|
|
# - i to input
|
|
|
|
# - q to quit entirely
|
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
# Let users debug the websocket if they want I guess
|
|
|
|
if config["websocket_trace"]:
|
|
|
|
websocket.enableTrace(True)
|
|
|
|
|
|
|
|
ws = websocket.WebSocketApp(context.websocket_endpoint())
|
|
|
|
# Might as well reuse the websocket object for my websocket context data (oops, is that bad?)
|
2023-05-01 15:21:45 +00:00
|
|
|
ws.context = context
|
2023-05-01 05:55:20 +00:00
|
|
|
ws.user_info = context.user_me()
|
|
|
|
ws.pause_output = False # Whether all output from the websocket should be paused (including status updates)
|
|
|
|
ws.output_buffer = [] # Individual print statements buffered from output.
|
|
|
|
ws.main_config = config
|
2023-05-01 15:21:45 +00:00
|
|
|
ws.current_room = 0
|
|
|
|
ws.current_room_data = False
|
|
|
|
|
|
|
|
# Go out and get the default room if one was provided.
|
|
|
|
if config["default_room"]:
|
|
|
|
try:
|
|
|
|
ws.current_room_data = context.get_by_id("content", config["default_room"])
|
|
|
|
ws.current_room = config["default_room"]
|
|
|
|
printr(Fore.GREEN + "Found default room %s" % ws.current_room_data["name"])
|
|
|
|
except Exception as ex:
|
|
|
|
printr(Fore.YELLOW + "Error searching for default room %d: %s" % (config["default_room"], ex))
|
2023-05-01 05:55:20 +00:00
|
|
|
|
|
|
|
# set the callback functions
|
|
|
|
ws.on_open = ws_onopen
|
|
|
|
ws.on_close = ws_onclose
|
|
|
|
ws.on_message = ws_onmessage
|
|
|
|
|
|
|
|
# connect to the WebSocket server and block until connected
|
|
|
|
ws.run_forever()
|
2023-05-01 04:34:56 +00:00
|
|
|
|
2023-04-30 22:18:25 +00:00
|
|
|
print("Program end")
|
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
|
|
|
|
def ws_onclose(ws):
|
|
|
|
print("Websocket closed! Program exit (FYI: you were in room %d)" % ws.current_room)
|
|
|
|
exit()
|
|
|
|
|
|
|
|
def ws_onopen(ws):
|
|
|
|
|
|
|
|
def main_loop():
|
|
|
|
printstatus = True
|
|
|
|
|
2023-05-01 15:21:45 +00:00
|
|
|
printr(Fore.GREEN + Style.BRIGHT + "\n-- Connected to live updates! --")
|
2023-05-01 06:21:55 +00:00
|
|
|
|
|
|
|
if not ws.current_room:
|
2023-05-01 15:21:45 +00:00
|
|
|
printr(Fore.YELLOW + "* You are not connected to any room! Press 'S' to search for a room! *")
|
2023-05-01 06:21:55 +00:00
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
# The infinite input loop! Or something!
|
|
|
|
while True:
|
|
|
|
if printstatus:
|
|
|
|
print_statusline(ws)
|
2023-05-01 14:35:31 +00:00
|
|
|
|
|
|
|
printstatus = True # Allow printing the statusline next time
|
|
|
|
ws.pause_output = False # Allow arbitrary output again
|
2023-05-01 05:55:20 +00:00
|
|
|
key = readchar.readkey()
|
|
|
|
|
|
|
|
# # Oops, websocket is not connected but you asked for a command that requires websocket!
|
|
|
|
# if not ws_context.connected and key in ["s", "g", "u", "i"]:
|
|
|
|
# print("No websocket connection")
|
|
|
|
# continue
|
|
|
|
|
2023-05-01 14:35:31 +00:00
|
|
|
ws.pause_output = True # Disable output for the duration of input handling
|
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
if key == "h":
|
|
|
|
for key, value in commands.items():
|
|
|
|
print(" " + Style.BRIGHT + key + Style.NORMAL + " - " + value)
|
|
|
|
elif key == "s":
|
2023-05-01 14:35:31 +00:00
|
|
|
search(ws)
|
2023-05-01 05:55:20 +00:00
|
|
|
elif key == "g":
|
|
|
|
print("not yet")
|
|
|
|
elif key == "u":
|
2023-05-01 06:21:55 +00:00
|
|
|
if not ws.current_room:
|
|
|
|
print("You're not in a room! Can't check userlist!")
|
|
|
|
else:
|
|
|
|
print("not yet")
|
2023-05-01 05:55:20 +00:00
|
|
|
elif key == "i":
|
2023-05-01 06:21:55 +00:00
|
|
|
if not ws.current_room:
|
|
|
|
print("You're not in a room! Can't send messages!")
|
|
|
|
else:
|
|
|
|
print("not yet")
|
2023-05-01 05:55:20 +00:00
|
|
|
elif key == "q":
|
2023-05-01 06:21:55 +00:00
|
|
|
print("Quitting (may take a bit for the websocket to close)")
|
2023-05-01 05:55:20 +00:00
|
|
|
ws.close()
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
printstatus = False
|
|
|
|
|
|
|
|
# create a thread to run the blocking task
|
|
|
|
thread = threading.Thread(target=main_loop)
|
|
|
|
thread.start()
|
|
|
|
|
2023-05-01 14:35:31 +00:00
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
def ws_onmessage(ws, message):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-05-01 00:46:08 +00:00
|
|
|
# Loads the config from file into the global config var. If the file
|
|
|
|
# doesn't exist, the file is created from the defaults in config.
|
|
|
|
# The function returns nothing
|
|
|
|
def load_or_create_global_config():
|
|
|
|
global config
|
|
|
|
# Check if the config file exists
|
2023-05-01 01:58:02 +00:00
|
|
|
if os.path.isfile(CONFIGFILE):
|
2023-05-01 00:46:08 +00:00
|
|
|
# Read and deserialize the config file
|
|
|
|
with open(CONFIGFILE, 'r', encoding='utf-8') as f:
|
2023-05-01 01:12:14 +00:00
|
|
|
temp_config = toml.load(f)
|
|
|
|
myutils.merge_dictionary(temp_config, config)
|
2023-05-01 00:46:08 +00:00
|
|
|
else:
|
|
|
|
# Serialize and write the config dictionary to the config file
|
|
|
|
logging.warn("No config found at " + CONFIGFILE + ", creating now")
|
|
|
|
with open(CONFIGFILE, 'w', encoding='utf-8') as f:
|
|
|
|
toml.dump(config, f)
|
|
|
|
|
2023-05-01 01:12:14 +00:00
|
|
|
myutils.set_logging_level(config["default_loglevel"])
|
|
|
|
|
2023-05-01 14:35:31 +00:00
|
|
|
# Enter a search loop which will repeat until you quit. Output should be PAUSED here
|
|
|
|
# (but someone else does it for us, we don't even know what 'pausing' is)
|
|
|
|
def search(ws):
|
|
|
|
while True:
|
|
|
|
searchterm = input("Search text (#ROOMNUM = set room, # to quit): ")
|
|
|
|
if searchterm == "#":
|
|
|
|
return
|
|
|
|
match = re.match(r'#(\d+)', searchterm)
|
|
|
|
if match:
|
2023-05-01 15:21:45 +00:00
|
|
|
roomid = int(match.group(1))
|
|
|
|
try:
|
|
|
|
ws.current_room_data = ws.context.get_by_id("content", roomid)
|
|
|
|
ws.current_room = roomid
|
|
|
|
print(Fore.GREEN + "Set room to %s" % ws.current_room_data["name"] + Style.RESET_ALL)
|
|
|
|
return
|
|
|
|
except Exception as ex:
|
|
|
|
print(Fore.RED + "Couldn't find room with id %d" % roomid + Style.RESET_ALL)
|
|
|
|
elif searchterm:
|
|
|
|
# Go search for rooms and display them
|
|
|
|
result = ws.context.basic_search(searchterm)["objects"]["content"]
|
|
|
|
if len(result):
|
|
|
|
for content in result:
|
|
|
|
printr(Style.BRIGHT + "%7s" % ("#%d" % content["id"]) + Style.RESET_ALL + " - %s" % content["name"])
|
|
|
|
else:
|
|
|
|
printr(Style.DIM + " -- No results -- ")
|
2023-05-01 00:46:08 +00:00
|
|
|
|
2023-05-01 01:58:02 +00:00
|
|
|
# Either pull the token from a file, or get the login from the command
|
|
|
|
# line if that doesn't work. WILL test your token against the real API
|
|
|
|
# even if it's pulled from file!
|
|
|
|
def authenticate(config, context: contentapi.ApiContext):
|
|
|
|
message = "No token file found"
|
|
|
|
if os.path.isfile(config["tokenfile"]):
|
|
|
|
with open(config["tokenfile"], 'r') as f:
|
|
|
|
token = f.read()
|
2023-05-01 02:08:00 +00:00
|
|
|
logging.debug("Token from file: " + token)
|
2023-05-01 02:13:01 +00:00
|
|
|
context.token = token
|
|
|
|
if context.is_token_valid():
|
2023-05-01 01:58:02 +00:00
|
|
|
logging.info("Logged in using token file " + config["tokenfile"])
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
message = "Token file expired"
|
|
|
|
|
2023-05-01 02:08:00 +00:00
|
|
|
message += ", Please enter login for " + config["api"]
|
|
|
|
|
2023-05-01 01:58:02 +00:00
|
|
|
while True:
|
2023-05-01 02:08:00 +00:00
|
|
|
print(message)
|
2023-05-01 01:58:02 +00:00
|
|
|
username = input("Username: ")
|
|
|
|
password = getpass.getpass("Password: ")
|
|
|
|
try:
|
|
|
|
token = context.login(username, password, config["expire_seconds"])
|
|
|
|
with open(config["tokenfile"], 'w') as f:
|
|
|
|
f.write(token)
|
|
|
|
logging.info("Token accepted, written to " + config["tokenfile"])
|
|
|
|
context.token = token
|
2023-05-01 02:08:00 +00:00
|
|
|
return
|
2023-05-01 01:58:02 +00:00
|
|
|
except Exception as ex:
|
2023-05-01 02:08:00 +00:00
|
|
|
print("ERROR: %s" % ex)
|
|
|
|
message = "Please try logging in again:"
|
2023-05-01 01:58:02 +00:00
|
|
|
|
2023-05-01 05:55:20 +00:00
|
|
|
def print_statusline(ws):
|
|
|
|
# if ws_context.connected: bg = Back.GREEN else: bg = Back.RED
|
2023-05-01 14:35:31 +00:00
|
|
|
if ws.current_room:
|
|
|
|
name = ws.current_room_data["name"]
|
2023-05-01 15:21:45 +00:00
|
|
|
room = "'" + (name[:(MAXTITLE - 3)] + '...' if len(name) > MAXTITLE else name) + "'"
|
2023-05-01 14:35:31 +00:00
|
|
|
else:
|
|
|
|
room = Fore.RED + Style.DIM + "NONE" + Style.NORMAL + Fore.BLACK
|
|
|
|
print(Back.GREEN + Fore.BLACK + "\n " + ws.user_info["username"] + " - " + room + " CTRL: h s g u i q " + Style.RESET_ALL)
|
2023-05-01 01:58:02 +00:00
|
|
|
|
2023-05-01 15:21:45 +00:00
|
|
|
# Print and then reset the style
|
|
|
|
def printr(msg):
|
|
|
|
print(msg + Style.RESET_ALL)
|
|
|
|
|
2023-04-30 22:18:25 +00:00
|
|
|
# Because python reasons
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|