Control socket: overhaul finished for now
This unbreaks the commands broken by the last commit. Everything is still better.
このコミットが含まれているのは:
コミット
5c8062466d
|
@ -1,17 +1,17 @@
|
||||||
from anonstream.control.spec import NoParse, Ambiguous, Parsed
|
from anonstream.control.spec import NoParse, Ambiguous, Parsed
|
||||||
from anonstream.control.spec.common import Str
|
from anonstream.control.spec.common import Str
|
||||||
#from anonstream.control.spec.methods.chat import SPEC as SPEC_CHAT
|
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.exit import SPEC as SPEC_EXIT
|
||||||
from anonstream.control.spec.methods.help import SPEC as SPEC_HELP
|
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.title import SPEC as SPEC_TITLE
|
||||||
#from anonstream.control.spec.methods.user import SPEC as SPEC_USER
|
from anonstream.control.spec.methods.user import SPEC as SPEC_USER
|
||||||
|
|
||||||
SPEC = Str({
|
SPEC = Str({
|
||||||
'help': SPEC_HELP,
|
'help': SPEC_HELP,
|
||||||
'exit': SPEC_EXIT,
|
'exit': SPEC_EXIT,
|
||||||
#'title': SPEC_TITLE,
|
'title': SPEC_TITLE,
|
||||||
#'chat': SPEC_CHAT,
|
'chat': SPEC_CHAT,
|
||||||
#'user': SPEC_USER,
|
'user': SPEC_USER,
|
||||||
})
|
})
|
||||||
|
|
||||||
async def parse(request):
|
async def parse(request):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from anonstream.control.exceptions import Exit
|
from anonstream.control.exceptions import Exit, Fail
|
||||||
from anonstream.control.parse import parse
|
from anonstream.control.parse import parse
|
||||||
|
|
||||||
def start_control_server_at(address):
|
def start_control_server_at(address):
|
||||||
|
@ -15,6 +15,8 @@ async def serve_control_client(reader, writer):
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
normal, response = await parse(request)
|
normal, response = await parse(request)
|
||||||
|
except Fail as e:
|
||||||
|
normal, response = None, e.args[0] + '\n'
|
||||||
except Exit:
|
except Exit:
|
||||||
writer.close()
|
writer.close()
|
||||||
break
|
break
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import json
|
||||||
|
|
||||||
from anonstream.control.spec import Spec, NoParse, Ambiguous, Parsed
|
from anonstream.control.spec import Spec, NoParse, Ambiguous, Parsed
|
||||||
from anonstream.control.spec.utils import get_item, startswith
|
from anonstream.control.spec.utils import get_item, startswith
|
||||||
|
|
||||||
|
@ -18,15 +20,15 @@ class Str(Spec):
|
||||||
reason = f'incomplete: expected one of {set(self.directives)}'
|
reason = f'incomplete: expected one of {set(self.directives)}'
|
||||||
else:
|
else:
|
||||||
reason = (
|
reason = (
|
||||||
f'bad word at position {index}: '
|
f'bad word at position {index} {word!r}: '
|
||||||
f'expected one of {set(self.directives)}, found {word!r}'
|
f'expected one of {set(self.directives)}'
|
||||||
)
|
)
|
||||||
raise NoParse(reason) from e
|
raise NoParse(reason) from e
|
||||||
else:
|
else:
|
||||||
if len(candidates) > 1:
|
if len(candidates) > 1:
|
||||||
raise Ambiguous(
|
raise Ambiguous(
|
||||||
f'bad word at position {index}: cannot unambiguously '
|
f'bad word at position {index} {word!r}: ambiguous '
|
||||||
f'match {word!r} against {set(self.directives)}'
|
f'abbreviation: {set(candidates)}'
|
||||||
)
|
)
|
||||||
return self.directives[directive], 1, []
|
return self.directives[directive], 1, []
|
||||||
|
|
||||||
|
@ -37,4 +39,65 @@ class End(Spec):
|
||||||
def consume(self, words, index):
|
def consume(self, words, index):
|
||||||
if len(words) <= index:
|
if len(words) <= index:
|
||||||
raise Parsed(self.fn)
|
raise Parsed(self.fn)
|
||||||
raise NoParse(f'garbage at position {index}: {words[index:]!r}')
|
raise NoParse(f'garbage at position {index} {words[index:]!r}')
|
||||||
|
|
||||||
|
class Args(Spec):
|
||||||
|
def __init__(self, spec):
|
||||||
|
self.spec = spec
|
||||||
|
|
||||||
|
class ArgsInt(Args):
|
||||||
|
def consume(self, words, index):
|
||||||
|
try:
|
||||||
|
n_string = words[index]
|
||||||
|
except IndexError:
|
||||||
|
raise NoParse(f'incomplete: expected integer')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
n = int(n_string)
|
||||||
|
except ValueError:
|
||||||
|
raise NoParse(
|
||||||
|
f'bad argument at position {index} {n_string!r}: '
|
||||||
|
f'could not decode base-10 integer'
|
||||||
|
)
|
||||||
|
return self.spec, 1, [n]
|
||||||
|
|
||||||
|
class ArgsString(Args):
|
||||||
|
def consume(self, words, index):
|
||||||
|
try:
|
||||||
|
string = words[index]
|
||||||
|
except IndexError:
|
||||||
|
raise NoParse(f'incomplete: expected string')
|
||||||
|
return self.spec, 1, [string]
|
||||||
|
|
||||||
|
class ArgsJson(Args):
|
||||||
|
def assert_obj(self, index, obj_json, obj):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transform_obj(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def consume(self, words, index):
|
||||||
|
try:
|
||||||
|
obj_json = words[index]
|
||||||
|
except IndexError:
|
||||||
|
raise NoParse(f'incomplete: expected json')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
obj = json.loads(obj_json)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise NoParse(
|
||||||
|
f'bad argument at position {index} {obj_json!r}: '
|
||||||
|
f'could not decode json'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assert_obj(index, obj_json, obj)
|
||||||
|
obj = self.transform_obj(obj)
|
||||||
|
return self.spec, 1, [obj]
|
||||||
|
|
||||||
|
class ArgsJsonString(ArgsJson):
|
||||||
|
def assert_obj(self, index, obj_json, obj):
|
||||||
|
if not isinstance(obj, str):
|
||||||
|
raise NoParse(
|
||||||
|
f'bad argument at position {index} {obj_json!r}: '
|
||||||
|
f'could not decode json string'
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from anonstream.chat import delete_chat_messages
|
||||||
|
from anonstream.control.spec import NoParse
|
||||||
|
from anonstream.control.spec.common import Str, End, Args
|
||||||
|
from anonstream.control.spec.utils import get_item, json_dumps_contiguous
|
||||||
|
|
||||||
|
class ArgsSeqs(Args):
|
||||||
|
def consume(self, words, index):
|
||||||
|
seqs = []
|
||||||
|
for i in itertools.count():
|
||||||
|
seq_string = get_item(index + i, words)
|
||||||
|
try:
|
||||||
|
seq = int(seq_string)
|
||||||
|
except TypeError as e:
|
||||||
|
if not seqs:
|
||||||
|
raise NoParse('incomplete: expected SEQ') from e
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except ValueError as e:
|
||||||
|
raise NoParse(
|
||||||
|
'could not decode {word!r} as base-10 integer'
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
seqs.append(seq)
|
||||||
|
return self.spec, i + 1, seqs
|
||||||
|
|
||||||
|
async def cmd_chat_help():
|
||||||
|
normal = ['chat', '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'
|
||||||
|
)
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_chat_delete(*seqs):
|
||||||
|
delete_chat_messages(seqs)
|
||||||
|
normal = ['chat', 'delete', *map(str, seqs)]
|
||||||
|
response = ''
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
SPEC = Str({
|
||||||
|
None: End(cmd_chat_help),
|
||||||
|
'help': End(cmd_chat_help),
|
||||||
|
'delete': ArgsSeqs(End(cmd_chat_delete)),
|
||||||
|
})
|
|
@ -0,0 +1,40 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from anonstream.control.exceptions import Fail
|
||||||
|
from anonstream.control.spec import Spec, NoParse
|
||||||
|
from anonstream.control.spec.common import Str, End, ArgsJsonString
|
||||||
|
from anonstream.control.spec.utils import get_item, json_dumps_contiguous
|
||||||
|
from anonstream.stream import get_stream_title, set_stream_title
|
||||||
|
|
||||||
|
async def cmd_title_help():
|
||||||
|
normal = ['title', '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 string, whitespace must be \\uXXXX-escaped\n'
|
||||||
|
)
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_title_show():
|
||||||
|
normal = ['title', 'show']
|
||||||
|
response = json.dumps(await get_stream_title()) + '\n'
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_title_set(title):
|
||||||
|
try:
|
||||||
|
await set_stream_title(title)
|
||||||
|
except OSError as e:
|
||||||
|
raise Fail(f'could not set title: {e}') from e
|
||||||
|
normal = ['title', 'set', json_dumps_contiguous(title)]
|
||||||
|
response = ''
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
SPEC = Str({
|
||||||
|
None: End(cmd_title_show),
|
||||||
|
'help': End(cmd_title_help),
|
||||||
|
'show': End(cmd_title_show),
|
||||||
|
'set': ArgsJsonString(End(cmd_title_set)),
|
||||||
|
})
|
|
@ -0,0 +1,135 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from quart import current_app
|
||||||
|
|
||||||
|
from anonstream.control.exceptions import Fail
|
||||||
|
from anonstream.control.spec import NoParse
|
||||||
|
from anonstream.control.spec.common import Str, End, ArgsInt, ArgsString, ArgsJson, ArgsJsonString
|
||||||
|
from anonstream.control.spec.utils import get_item, 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
|
||||||
|
|
||||||
|
class ArgsJsonTokenUser(ArgsJsonString):
|
||||||
|
def transform_obj(self, token):
|
||||||
|
try:
|
||||||
|
user = USERS_BY_TOKEN[token]
|
||||||
|
except KeyError:
|
||||||
|
raise NoParse(f'no user with token {token!r}')
|
||||||
|
return user
|
||||||
|
|
||||||
|
def ArgsUser(spec):
|
||||||
|
return Str({
|
||||||
|
'token': ArgsJsonTokenUser(spec),
|
||||||
|
#'hash': ArgsJsonHashUser(spec),
|
||||||
|
})
|
||||||
|
|
||||||
|
async def cmd_user_help():
|
||||||
|
normal = ['user', '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, json string\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'
|
||||||
|
)
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_user_show():
|
||||||
|
normal = ['user', 'show']
|
||||||
|
response = json.dumps(tuple(USERS_BY_TOKEN)) + '\n'
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_user_attr(user):
|
||||||
|
normal = ['user', 'attr', 'token', json_dumps_contiguous(user['token'])]
|
||||||
|
response = json.dumps(tuple(user.keys())) + '\n'
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_user_get(user, attr):
|
||||||
|
try:
|
||||||
|
value = user[attr]
|
||||||
|
except KeyError as e:
|
||||||
|
raise Fail('user has no such attribute') from e
|
||||||
|
try:
|
||||||
|
value_json = json.dumps(value)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise Fail('value is not representable in json') from e
|
||||||
|
normal = [
|
||||||
|
'user',
|
||||||
|
'get',
|
||||||
|
'token',
|
||||||
|
json_dumps_contiguous(user['token']),
|
||||||
|
attr,
|
||||||
|
]
|
||||||
|
response = value_json + '\n'
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_user_set(user, attr, value):
|
||||||
|
if attr not in user:
|
||||||
|
raise Fail(f'user has no attribute {attr!r}')
|
||||||
|
user[attr] = value
|
||||||
|
if attr in USER_WEBSOCKET_ATTRS:
|
||||||
|
USERS_UPDATE_BUFFER.add(user['token'])
|
||||||
|
normal = [
|
||||||
|
'user',
|
||||||
|
'set',
|
||||||
|
'token',
|
||||||
|
json_dumps_contiguous(user['token']),
|
||||||
|
attr,
|
||||||
|
json_dumps_contiguous(value),
|
||||||
|
]
|
||||||
|
response = ''
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_user_eyes_show(user):
|
||||||
|
normal = [
|
||||||
|
'user',
|
||||||
|
'eyes',
|
||||||
|
'token',
|
||||||
|
json_dumps_contiguous(user['token']),
|
||||||
|
'show'
|
||||||
|
]
|
||||||
|
response = json.dumps(user['eyes']['current']) + '\n'
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
async def cmd_user_eyes_delete(user, eyes_id):
|
||||||
|
try:
|
||||||
|
user['eyes']['current'].pop(eyes_id)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
normal = [
|
||||||
|
'user',
|
||||||
|
'eyes',
|
||||||
|
'token',
|
||||||
|
json_dumps_contiguous(user['token']),
|
||||||
|
'delete',
|
||||||
|
str(eyes_id),
|
||||||
|
]
|
||||||
|
response = ''
|
||||||
|
return normal, response
|
||||||
|
|
||||||
|
SPEC = Str({
|
||||||
|
None: End(cmd_user_show),
|
||||||
|
'help': End(cmd_user_help),
|
||||||
|
'show': End(cmd_user_show),
|
||||||
|
'attr': ArgsUser(End(cmd_user_attr)),
|
||||||
|
'get': ArgsUser(ArgsString(End(cmd_user_get))),
|
||||||
|
'set': ArgsUser(ArgsString(ArgsJson(End(cmd_user_set)))),
|
||||||
|
'eyes': ArgsUser(Str({
|
||||||
|
None: End(cmd_user_eyes_show),
|
||||||
|
'show': End(cmd_user_eyes_show),
|
||||||
|
'delete': ArgsInt(End(cmd_user_eyes_delete)),
|
||||||
|
})),
|
||||||
|
})
|
|
@ -1,3 +1,5 @@
|
||||||
|
import json
|
||||||
|
|
||||||
def get_item(index, words):
|
def get_item(index, words):
|
||||||
try:
|
try:
|
||||||
word = words[index]
|
word = words[index]
|
||||||
|
@ -8,6 +10,9 @@ def get_item(index, words):
|
||||||
raise NoParse(f'empty word at position {index}')
|
raise NoParse(f'empty word at position {index}')
|
||||||
return word
|
return word
|
||||||
|
|
||||||
|
def json_dumps_contiguous(obj, **kwargs):
|
||||||
|
return json.dumps(obj, **kwargs).replace(' ', r'\u0020')
|
||||||
|
|
||||||
def startswith(string, prefix):
|
def startswith(string, prefix):
|
||||||
if string is None or prefix is None:
|
if string is None or prefix is None:
|
||||||
return string is prefix
|
return string is prefix
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
def json_dumps_contiguous(obj, **kwargs):
|
|
||||||
return json.dumps(obj, **kwargs).replace(' ', r'\u0020')
|
|
読み込み中…
新しいイシューから参照