Control socket: overhaul implementation
This breaks some commands, everything else is better though.
このコミットが含まれているのは:
コミット
abfa3fe865
|
@ -1,44 +0,0 @@
|
|||
from anonstream.control.commands.help import *
|
||||
from anonstream.control.commands.exit import *
|
||||
from anonstream.control.commands.title import *
|
||||
from anonstream.control.commands.chat import *
|
||||
from anonstream.control.commands.user import *
|
||||
|
||||
METHOD_HELP = 'help'
|
||||
METHOD_EXIT = 'exit'
|
||||
METHOD_TITLE = 'title'
|
||||
METHOD_CHAT = 'chat'
|
||||
METHOD_USER = 'user'
|
||||
|
||||
METHOD_COMMAND_FUNCTIONS = {
|
||||
METHOD_HELP: {
|
||||
None: command_help,
|
||||
'help': command_help_help,
|
||||
},
|
||||
METHOD_EXIT: {
|
||||
None: command_exit,
|
||||
'help': command_exit_help,
|
||||
},
|
||||
METHOD_TITLE: {
|
||||
None: command_title_show,
|
||||
'help': command_title_help,
|
||||
'show': command_title_show,
|
||||
'set': command_title_set,
|
||||
},
|
||||
METHOD_CHAT: {
|
||||
None: command_chat_help,
|
||||
'help': command_chat_help,
|
||||
'delete': command_chat_delete,
|
||||
},
|
||||
METHOD_USER: {
|
||||
None: command_user_show,
|
||||
'help': command_user_help,
|
||||
'show': command_user_show,
|
||||
'attr': command_user_attr,
|
||||
'get': command_user_get,
|
||||
'set': command_user_set,
|
||||
'eyes': command_user_eyes,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
from anonstream.control.exceptions import BadArgument, Incomplete, Garbage
|
||||
from anonstream.chat import delete_chat_messages
|
||||
|
||||
async def command_chat_help(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = ['help']
|
||||
response = (
|
||||
'Usage: chat {show [MESSAGES] | delete SEQS}\n'
|
||||
'Commands:\n'
|
||||
#' chat show [MESSAGES]......show chat messages\n'
|
||||
' chat delete SEQS..........delete chat messages\n'
|
||||
'Definitions:\n'
|
||||
#' MESSAGES..................undefined\n'
|
||||
' SEQS......................=SEQ [SEQ...]\n'
|
||||
' SEQ.......................a chat message\'s seq, base-10 integer\n'
|
||||
)
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_chat_delete(args):
|
||||
match args:
|
||||
case []:
|
||||
raise Incomplete
|
||||
case _:
|
||||
try:
|
||||
seqs = list(map(int, args))
|
||||
except ValueError as e:
|
||||
raise BadArgument('SEQ must be a base-10 integer') from e
|
||||
delete_chat_messages(seqs)
|
||||
normal_options = ['delete', *map(str, seqs)]
|
||||
response = ''
|
||||
return normal_options, response
|
|
@ -1,20 +0,0 @@
|
|||
from anonstream.control.exceptions import Exit, Garbage
|
||||
|
||||
async def command_exit(args):
|
||||
match args:
|
||||
case []:
|
||||
raise Exit
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
|
||||
async def command_exit_help(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = ['help']
|
||||
response = (
|
||||
'Usage: exit\n'
|
||||
'close the connection\n'
|
||||
)
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
|
@ -1,38 +0,0 @@
|
|||
from anonstream.control.exceptions import Garbage
|
||||
|
||||
async def command_help(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = []
|
||||
response = (
|
||||
'Usage: METHOD [COMMAND | help]\n'
|
||||
'Examples:\n'
|
||||
' help...........................show this help message\n'
|
||||
' exit...........................close the control connection\n'
|
||||
' title [show]...................show the stream title\n'
|
||||
' title set TITLE................set the stream title\n'
|
||||
' user [show]....................show a list of users\n'
|
||||
' user attr USER.................set an attribute of a user\n'
|
||||
' user get USER ATTR.............set an attribute of a user\n'
|
||||
' user set USER ATTR VALUE.......set an attribute of a user\n'
|
||||
#' user kick USERS [FAREWELL].....kick users\n'
|
||||
' user eyes USER [show]..........show a list of active video responses\n'
|
||||
' user eyes USER delete EYES_ID..end a video response\n'
|
||||
#' chat show MESSAGES.............show a list of messages\n'
|
||||
' chat delete SEQS...............delete a set of messages\n'
|
||||
)
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_help_help(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = ['help']
|
||||
response = (
|
||||
'Usage: help\n'
|
||||
'show usage syntax and examples\n'
|
||||
)
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
|
@ -1,53 +0,0 @@
|
|||
import json
|
||||
|
||||
from anonstream.control.exceptions import BadArgument, Incomplete, Garbage, Failed
|
||||
from anonstream.control.utils import json_dumps_contiguous
|
||||
from anonstream.stream import get_stream_title, set_stream_title
|
||||
|
||||
async def command_title_help(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = ['help']
|
||||
response = (
|
||||
'Usage: title [show | set TITLE]\n'
|
||||
'Commands:\n'
|
||||
' title [show].......show the stream title\n'
|
||||
' title set TITLE....set the stream title to TITLE\n'
|
||||
'Definitions:\n'
|
||||
' TITLE..............a json-encoded string, whitespace must be \\uXXXX-escaped\n'
|
||||
)
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_title_show(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = ['show']
|
||||
response = json.dumps(await get_stream_title()) + '\n'
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_title_set(args):
|
||||
match args:
|
||||
case [title_json]:
|
||||
try:
|
||||
title = json.loads(title_json)
|
||||
except json.JSONDecodeError as e:
|
||||
raise BadArgument('could not decode json')
|
||||
else:
|
||||
if not isinstance(title, str):
|
||||
raise BadArgument('could not decode json as string')
|
||||
else:
|
||||
try:
|
||||
await set_stream_title(title)
|
||||
except OSError as e:
|
||||
raise Failed(str(e)) from e
|
||||
normal_options = ['set', json_dumps_contiguous(title)]
|
||||
response = ''
|
||||
case []:
|
||||
raise Incomplete
|
||||
case [_, *garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
|
@ -1,153 +0,0 @@
|
|||
import json
|
||||
|
||||
from quart import current_app
|
||||
|
||||
from anonstream.control.exceptions import BadArgument, Incomplete, Garbage, Failed
|
||||
from anonstream.control.utils import json_dumps_contiguous
|
||||
from anonstream.utils.user import USER_WEBSOCKET_ATTRS
|
||||
|
||||
USERS_BY_TOKEN = current_app.users_by_token
|
||||
USERS = current_app.users
|
||||
USERS_UPDATE_BUFFER = current_app.users_update_buffer
|
||||
|
||||
async def command_user_help(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = ['help']
|
||||
response = (
|
||||
'Usage: user [show | attr USER | get USER ATTR | set USER ATTR VALUE]\n'
|
||||
'Commands:\n'
|
||||
' user [show].......................show all users\' tokens\n'
|
||||
' user attr USER....................show names of a user\'s attributes\n'
|
||||
' user get USER ATTR................show an attribute of a user\n'
|
||||
' user set USER ATTR................set an attribute of a user\n'
|
||||
' user eyes USER [show].............show a user\'s active video responses\n'
|
||||
' user eyes USER delete EYES_ID.....end a video response to a user\n'
|
||||
'Definitions:\n'
|
||||
#' USER..............................={token TOKEN | hash HASH}\n'
|
||||
' USER..............................=token TOKEN\n'
|
||||
' TOKEN..............................a token\n'
|
||||
#' HASH..............................a token hash\n'
|
||||
' ATTR...............................a user attribute, re:[a-z0-9_]+\n'
|
||||
' EYES_ID............................a user\'s eyes_id, base 10 integer\n'
|
||||
)
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_user_show(args):
|
||||
match args:
|
||||
case []:
|
||||
normal_options = ['show']
|
||||
response = json.dumps(tuple(USERS_BY_TOKEN)) + '\n'
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_user_attr(args):
|
||||
match args:
|
||||
case []:
|
||||
raise Incomplete
|
||||
case ['token', token_json]:
|
||||
try:
|
||||
token = json.loads(token_json)
|
||||
except json.JSONDecodeError:
|
||||
raise BadArgument('could not decode TOKEN as json')
|
||||
try:
|
||||
user = USERS_BY_TOKEN[token]
|
||||
except KeyError:
|
||||
raise Failed(f"no user exists with token {token!r}, try 'user show'")
|
||||
normal_options = ['attr', 'token', json_dumps_contiguous(token)]
|
||||
response = json.dumps(tuple(user.keys())) + '\n'
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_user_get(args):
|
||||
match args:
|
||||
case ['token', token_json, attr]:
|
||||
try:
|
||||
token = json.loads(token_json)
|
||||
except json.JSONDecodeError:
|
||||
raise BadArgument('could not decode TOKEN as json')
|
||||
try:
|
||||
user = USERS_BY_TOKEN[token]
|
||||
except KeyError:
|
||||
raise Failed(f"no user exists with token {token!r}, try 'user show'")
|
||||
try:
|
||||
value = user[attr]
|
||||
except KeyError:
|
||||
raise Failed(f"user has no attribute {attr!r}, try 'user attr token {json_dumps_contiguous(token)}'")
|
||||
try:
|
||||
value_json = json.dumps(value)
|
||||
except TypeError:
|
||||
raise Failed(f'attribute {attr!r} is not JSON serializable')
|
||||
normal_options = ['get', 'token', json_dumps_contiguous(token), attr]
|
||||
response = value_json + '\n'
|
||||
case []:
|
||||
raise Incomplete
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_user_set(args):
|
||||
match args:
|
||||
case ['token', token_json, attr, value_json]:
|
||||
try:
|
||||
token = json.loads(token_json)
|
||||
except json.JSONDecodeError:
|
||||
raise BadArgument('could not decode TOKEN as json')
|
||||
try:
|
||||
user = USERS_BY_TOKEN[token]
|
||||
except KeyError:
|
||||
raise Failed(f"no user exists with token {token!r}, try 'user show'")
|
||||
try:
|
||||
value = user[attr]
|
||||
except KeyError:
|
||||
raise Failed(f"user has no attribute {attr!r}, try 'user attr token {json_dumps_contiguous(token)}")
|
||||
try:
|
||||
value = json.loads(value_json)
|
||||
except json.JSONDecodeError:
|
||||
raise Failed('could not decode json')
|
||||
user[attr] = value
|
||||
if attr in USER_WEBSOCKET_ATTRS:
|
||||
USERS_UPDATE_BUFFER.add(token)
|
||||
normal_options = ['set', 'token', json_dumps_contiguous(token), attr, json_dumps_contiguous(value)]
|
||||
response = ''
|
||||
case []:
|
||||
raise Incomplete
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
||||
|
||||
async def command_user_eyes(args):
|
||||
match args:
|
||||
case ['token', token_json, *subargs]:
|
||||
try:
|
||||
token = json.loads(token_json)
|
||||
except json.JSONDecodeError:
|
||||
raise BadArgument('could not decode TOKEN as json')
|
||||
try:
|
||||
user = USERS_BY_TOKEN[token]
|
||||
except KeyError:
|
||||
raise Failed(f"no user exists with token {token!r}, try 'user show'")
|
||||
match subargs:
|
||||
case [] | ['show']:
|
||||
normal_options = ['eyes', 'token', json_dumps_contiguous(token), 'show']
|
||||
response = json.dumps(user['eyes']['current']) + '\n'
|
||||
case ['delete', eyes_id_json]:
|
||||
try:
|
||||
eyes_id = json.loads(eyes_id_json)
|
||||
except json.JSONDecodeError:
|
||||
raise BadArgument('could not decode EYES_ID as json')
|
||||
try:
|
||||
user['eyes']['current'].pop(eyes_id)
|
||||
except KeyError:
|
||||
pass
|
||||
normal_options = ['eyes', 'token', json_dumps_contiguous(token), 'delete', json_dumps_contiguous(eyes_id)]
|
||||
response = ''
|
||||
case []:
|
||||
raise Incomplete
|
||||
case [*garbage]:
|
||||
raise Garbage(garbage)
|
||||
return normal_options, response
|
|
@ -1,23 +1,5 @@
|
|||
class Exit(Exception):
|
||||
pass
|
||||
|
||||
class UnknownMethod(Exception):
|
||||
pass
|
||||
|
||||
class UnknownCommand(Exception):
|
||||
pass
|
||||
|
||||
class UnknownArgument(Exception):
|
||||
pass
|
||||
|
||||
class BadArgument(Exception):
|
||||
pass
|
||||
|
||||
class Incomplete(Exception):
|
||||
pass
|
||||
|
||||
class Garbage(Exception):
|
||||
pass
|
||||
|
||||
class Failed(Exception):
|
||||
class Fail(Exception):
|
||||
pass
|
||||
|
|
|
@ -1,61 +1,41 @@
|
|||
import json
|
||||
from anonstream.control.spec import NoParse, Ambiguous, Parsed
|
||||
from anonstream.control.spec.common import Str
|
||||
#from anonstream.control.spec.methods.chat import SPEC as SPEC_CHAT
|
||||
from anonstream.control.spec.methods.exit import SPEC as SPEC_EXIT
|
||||
from anonstream.control.spec.methods.help import SPEC as SPEC_HELP
|
||||
#from anonstream.control.spec.methods.title import SPEC as SPEC_TITLE
|
||||
#from anonstream.control.spec.methods.user import SPEC as SPEC_USER
|
||||
|
||||
from anonstream.control.exceptions import UnknownMethod, UnknownCommand, BadArgument, Incomplete, Garbage, Failed
|
||||
from anonstream.control.commands import METHOD_COMMAND_FUNCTIONS
|
||||
SPEC = Str({
|
||||
'help': SPEC_HELP,
|
||||
'exit': SPEC_EXIT,
|
||||
#'title': SPEC_TITLE,
|
||||
#'chat': SPEC_CHAT,
|
||||
#'user': SPEC_USER,
|
||||
})
|
||||
|
||||
async def parse_request(request):
|
||||
try:
|
||||
method, *options = request.split()
|
||||
except ValueError:
|
||||
normal, response = (None, []), ''
|
||||
async def parse(request):
|
||||
words = request.split()
|
||||
if not words:
|
||||
normal, response = None, ''
|
||||
else:
|
||||
spec = SPEC
|
||||
index = 0
|
||||
args = []
|
||||
while True:
|
||||
try:
|
||||
normal, response = await parse(method, options)
|
||||
except UnknownMethod as e:
|
||||
unknown_method, *_ = e.args
|
||||
normal = None
|
||||
response = f"method {unknown_method!r} is unknown, try 'help'\n"
|
||||
except UnknownCommand as e:
|
||||
method, unknown_command, *_ = e.args
|
||||
normal = None
|
||||
response = f"command {unknown_command!r} is unknown, try {f'{method} help'!r}\n"
|
||||
except BadArgument as e:
|
||||
reason, *_ = e.args
|
||||
normal = None
|
||||
response = f"{reason}, try {f'{method} help'!r}\n"
|
||||
except Incomplete as e:
|
||||
method, *_ = e.args
|
||||
normal = None
|
||||
response = f"command is incomplete, try {f'{method} help'!r}\n"
|
||||
except Garbage as e:
|
||||
garbage, *_ = e.args
|
||||
normal = None
|
||||
response = f"command has trailing garbage {garbage!r}, try {f'{method} help'!r}\n"
|
||||
except Failed as e:
|
||||
reason, *_ = e.args
|
||||
normal = None
|
||||
response = reason + '\n'
|
||||
return normal, response
|
||||
|
||||
async def parse(method, options):
|
||||
try:
|
||||
command, *args = options
|
||||
except ValueError:
|
||||
command, args = None, []
|
||||
try:
|
||||
functions = METHOD_COMMAND_FUNCTIONS[method]
|
||||
except KeyError:
|
||||
raise UnknownMethod(method)
|
||||
else:
|
||||
normal_method = method
|
||||
try:
|
||||
fn = functions[command]
|
||||
except KeyError:
|
||||
raise UnknownCommand(method, command)
|
||||
else:
|
||||
try:
|
||||
normal_options, response = await fn(args)
|
||||
except Incomplete as e:
|
||||
raise Incomplete(method) from e
|
||||
normal = (normal_method, normal_options)
|
||||
spec, n_consumed, more_args = spec.consume(words, index)
|
||||
except NoParse as e:
|
||||
normal, response = None, e.args[0] + '\n'
|
||||
break
|
||||
except Ambiguous as e:
|
||||
normal, response = None, e.args[0] + '\n'
|
||||
break
|
||||
except Parsed as e:
|
||||
fn, *_ = e.args
|
||||
normal, response = await fn(*args)
|
||||
break
|
||||
else:
|
||||
index += n_consumed
|
||||
args.extend(more_args)
|
||||
return normal, response
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
|
||||
from anonstream.control.exceptions import Exit
|
||||
from anonstream.control.parse import parse_request
|
||||
from anonstream.control.parse import parse
|
||||
|
||||
def start_control_server_at(address):
|
||||
return asyncio.start_unix_server(serve_control_client, address)
|
||||
|
@ -14,18 +14,16 @@ async def serve_control_client(reader, writer):
|
|||
normal, response = None, str(e)
|
||||
else:
|
||||
try:
|
||||
normal, response = await parse_request(request)
|
||||
normal, response = await parse(request)
|
||||
except Exit:
|
||||
writer.close()
|
||||
break
|
||||
|
||||
if normal is not None:
|
||||
normal_method, normal_options = normal
|
||||
if normal_method is not None:
|
||||
writer.write(normal_method.encode())
|
||||
for arg in normal_options:
|
||||
for index, word in enumerate(normal):
|
||||
if index > 0:
|
||||
writer.write(b' ')
|
||||
writer.write(arg.encode())
|
||||
writer.write(word.encode())
|
||||
writer.write(b'\n')
|
||||
elif response:
|
||||
writer.write(b'error: ')
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
class NoParse(Exception):
|
||||
pass
|
||||
|
||||
class Ambiguous(Exception):
|
||||
pass
|
||||
|
||||
class Parsed(Exception):
|
||||
pass
|
||||
|
||||
class Spec:
|
||||
def consume(self, words, index):
|
||||
raise NotImplemented
|
|
@ -0,0 +1,40 @@
|
|||
from anonstream.control.spec import Spec, NoParse, Ambiguous, Parsed
|
||||
from anonstream.control.spec.utils import get_item, startswith
|
||||
|
||||
class Str(Spec):
|
||||
def __init__(self, directives):
|
||||
self.directives = directives
|
||||
|
||||
def consume(self, words, index):
|
||||
word = get_item(index, words)
|
||||
candidates = tuple(filter(
|
||||
lambda directive: startswith(directive, word),
|
||||
self.directives,
|
||||
))
|
||||
try:
|
||||
directive = candidates[0]
|
||||
except IndexError as e:
|
||||
if word is None:
|
||||
reason = f'incomplete: expected one of {set(self.directives)}'
|
||||
else:
|
||||
reason = (
|
||||
f'bad word at position {index}: '
|
||||
f'expected one of {set(self.directives)}, found {word!r}'
|
||||
)
|
||||
raise NoParse(reason) from e
|
||||
else:
|
||||
if len(candidates) > 1:
|
||||
raise Ambiguous(
|
||||
f'bad word at position {index}: cannot unambiguously '
|
||||
f'match {word!r} against {set(self.directives)}'
|
||||
)
|
||||
return self.directives[directive], 1, []
|
||||
|
||||
class End(Spec):
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
|
||||
def consume(self, words, index):
|
||||
if len(words) <= index:
|
||||
raise Parsed(self.fn)
|
||||
raise NoParse(f'garbage at position {index}: {words[index:]!r}')
|
|
@ -0,0 +1,18 @@
|
|||
from anonstream.control.spec.common import Str, End
|
||||
from anonstream.control.exceptions import Exit
|
||||
|
||||
async def cmd_exit():
|
||||
raise Exit
|
||||
|
||||
async def cmd_exit_help():
|
||||
normal = ['exit', 'help']
|
||||
response = (
|
||||
'Usage: exit\n'
|
||||
'close the connection\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
SPEC = Str({
|
||||
None: End(cmd_exit),
|
||||
'help': End(cmd_exit_help),
|
||||
})
|
|
@ -0,0 +1,35 @@
|
|||
from anonstream.control.spec.common import Str, End
|
||||
|
||||
async def cmd_help():
|
||||
normal = ['help']
|
||||
response = (
|
||||
'Usage: METHOD [COMMAND | help]\n'
|
||||
'Examples:\n'
|
||||
' help...........................show this help message\n'
|
||||
' exit...........................close the control connection\n'
|
||||
' title [show]...................show the stream title\n'
|
||||
' title set TITLE................set the stream title\n'
|
||||
' user [show]....................show a list of users\n'
|
||||
' user attr USER.................set an attribute of a user\n'
|
||||
' user get USER ATTR.............set an attribute of a user\n'
|
||||
' user set USER ATTR VALUE.......set an attribute of a user\n'
|
||||
#' user kick USERS [FAREWELL].....kick users\n'
|
||||
' user eyes USER [show]..........show a list of active video responses\n'
|
||||
' user eyes USER delete EYES_ID..end a video response\n'
|
||||
#' chat show MESSAGES.............show a list of messages\n'
|
||||
' chat delete SEQS...............delete a set of messages\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
async def cmd_help_help():
|
||||
normal = ['help', 'help']
|
||||
response = (
|
||||
'Usage: help\n'
|
||||
'show usage syntax and examples\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
SPEC = Str({
|
||||
None: End(cmd_help),
|
||||
'help': End(cmd_help_help),
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
def get_item(index, words):
|
||||
try:
|
||||
word = words[index]
|
||||
except IndexError:
|
||||
word = None
|
||||
else:
|
||||
if not word:
|
||||
raise NoParse(f'empty word at position {index}')
|
||||
return word
|
||||
|
||||
def startswith(string, prefix):
|
||||
if string is None or prefix is None:
|
||||
return string is prefix
|
||||
return string.startswith(prefix)
|
読み込み中…
新しいイシューから参照