Control socket: overhaul finished for now

This unbreaks the commands broken by the last commit. Everything is
still better.
このコミットが含まれているのは:
n9k 2022-06-15 08:49:42 +00:00
コミット 5c8062466d
8個のファイルの変更309行の追加16行の削除

ファイルの表示

@ -1,17 +1,17 @@
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.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.spec.methods.title import SPEC as SPEC_TITLE
from anonstream.control.spec.methods.user import SPEC as SPEC_USER
SPEC = Str({
'help': SPEC_HELP,
'exit': SPEC_EXIT,
#'title': SPEC_TITLE,
#'chat': SPEC_CHAT,
#'user': SPEC_USER,
'title': SPEC_TITLE,
'chat': SPEC_CHAT,
'user': SPEC_USER,
})
async def parse(request):

ファイルの表示

@ -1,6 +1,6 @@
import asyncio
from anonstream.control.exceptions import Exit
from anonstream.control.exceptions import Exit, Fail
from anonstream.control.parse import parse
def start_control_server_at(address):
@ -15,6 +15,8 @@ async def serve_control_client(reader, writer):
else:
try:
normal, response = await parse(request)
except Fail as e:
normal, response = None, e.args[0] + '\n'
except Exit:
writer.close()
break

ファイルの表示

@ -1,3 +1,5 @@
import json
from anonstream.control.spec import Spec, NoParse, Ambiguous, Parsed
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)}'
else:
reason = (
f'bad word at position {index}: '
f'expected one of {set(self.directives)}, found {word!r}'
f'bad word at position {index} {word!r}: '
f'expected one of {set(self.directives)}'
)
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)}'
f'bad word at position {index} {word!r}: ambiguous '
f'abbreviation: {set(candidates)}'
)
return self.directives[directive], 1, []
@ -37,4 +39,65 @@ class End(Spec):
def consume(self, words, index):
if len(words) <= index:
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'
)

52
anonstream/control/spec/methods/chat.py ノーマルファイル
ファイルの表示

@ -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)),
})

40
anonstream/control/spec/methods/title.py ノーマルファイル
ファイルの表示

@ -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)),
})

135
anonstream/control/spec/methods/user.py ノーマルファイル
ファイルの表示

@ -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):
try:
word = words[index]
@ -8,6 +10,9 @@ def get_item(index, words):
raise NoParse(f'empty word at position {index}')
return word
def json_dumps_contiguous(obj, **kwargs):
return json.dumps(obj, **kwargs).replace(' ', r'\u0020')
def startswith(string, prefix):
if string is None or prefix is None:
return string is prefix

ファイルの表示

@ -1,4 +0,0 @@
import json
def json_dumps_contiguous(obj, **kwargs):
return json.dumps(obj, **kwargs).replace(' ', r'\u0020')