msgctl 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- msg/__init__.py +2 -0
- msg/admin/__init__.py +0 -0
- msg/admin/backup_retirement.py +56 -0
- msg/admin/backups.py +341 -0
- msg/admin/custodial_check.py +77 -0
- msg/admin/diagnostics.py +750 -0
- msg/admin/market.py +91 -0
- msg/admin/market_check.py +341 -0
- msg/admin/money.py +322 -0
- msg/admin/preflight.py +142 -0
- msg/admin/recovery_replay.py +281 -0
- msg/admin/restore_database.py +37 -0
- msg/admin/root.py +347 -0
- msg/admin/rotation.py +197 -0
- msg/admin/token_delivery_check.py +54 -0
- msg/admin/upgrade_check.py +58 -0
- msg/application.py +163 -0
- msg/bootstrap.py +305 -0
- msg/cli.py +537 -0
- msg/client.py +725 -0
- msg/client_certificates.py +98 -0
- msg/client_content.py +43 -0
- msg/client_custodial.py +206 -0
- msg/client_market.py +104 -0
- msg/client_recovery.py +186 -0
- msg/client_secrets.py +58 -0
- msg/client_tokens.py +99 -0
- msg/client_upgrade.py +188 -0
- msg/config.py +318 -0
- msg/constants.py +11 -0
- msg/core/__init__.py +0 -0
- msg/core/batching.py +23 -0
- msg/core/codec.py +253 -0
- msg/core/contracts.py +128 -0
- msg/core/cursors.py +68 -0
- msg/core/email_address.py +33 -0
- msg/core/errors.py +22 -0
- msg/core/events.py +6 -0
- msg/core/execution_ports.py +20 -0
- msg/core/executor.py +193 -0
- msg/core/models.py +517 -0
- msg/core/packet.py +71 -0
- msg/core/permissions.py +14 -0
- msg/core/query.py +44 -0
- msg/core/read_query.py +60 -0
- msg/core/registry.py +173 -0
- msg/core/requests.py +58 -0
- msg/core/schema_policy.py +28 -0
- msg/core/schemas.py +30 -0
- msg/core/tags.py +21 -0
- msg/core/template_dsl.py +94 -0
- msg/core/text_patch.py +283 -0
- msg/core/tool_execution.py +28 -0
- msg/core/transfer.py +183 -0
- msg/daemon.py +255 -0
- msg/data/__init__.py +0 -0
- msg/data/bootstrap.json +72 -0
- msg/data/favicon.png +0 -0
- msg/data/logo-dark.svg +5 -0
- msg/data/logo.svg +5 -0
- msg/data/recovery-checkpoint.example.json +1 -0
- msg/data/recovery-checkpoint.schema.json +118 -0
- msg/data/shortcodes.json +1 -0
- msg/data/system/AGENTS.md +6 -0
- msg/data/system/rules/_index.md +17 -0
- msg/data/system/rules/auth.md +6 -0
- msg/data/system/rules/files.md +6 -0
- msg/data/system/rules/identity.md +6 -0
- msg/data/system/rules/protocol.md +6 -0
- msg/data/system/rules/read-write.md +6 -0
- msg/data/system/rules/recovery.md +6 -0
- msg/data/system/rules/security.md +10 -0
- msg/data/system/rules/topics.md +6 -0
- msg/extensions/__init__.py +1 -0
- msg/extensions/hosting.py +259 -0
- msg/extensions/keystore.py +96 -0
- msg/extensions/repositories.py +769 -0
- msg/extensions/rss.py +67 -0
- msg/extensions/ssh.py +264 -0
- msg/extensions/ssh_git.py +300 -0
- msg/extensions/tools.py +150 -0
- msg/hosting_runtime.py +138 -0
- msg/market/__init__.py +1 -0
- msg/market/arbitration.py +481 -0
- msg/market/delivery.py +338 -0
- msg/market/delivery_notifications.py +110 -0
- msg/market/delivery_targets.py +76 -0
- msg/market/email.py +100 -0
- msg/market/escrow.py +345 -0
- msg/market/orders.py +218 -0
- msg/market/policy.py +147 -0
- msg/market/rationale.py +91 -0
- msg/market/references.py +19 -0
- msg/market/targets.py +157 -0
- msg/plugins/__init__.py +28 -0
- msg/plugins/achievements.py +316 -0
- msg/plugins/batch.py +32 -0
- msg/plugins/bounty.py +366 -0
- msg/plugins/collaboration.py +261 -0
- msg/plugins/common.py +217 -0
- msg/plugins/communication.py +853 -0
- msg/plugins/content.py +855 -0
- msg/plugins/custodial_lifecycle.py +224 -0
- msg/plugins/delivery.py +230 -0
- msg/plugins/discovery.py +1266 -0
- msg/plugins/discussion.py +162 -0
- msg/plugins/extensions.py +10 -0
- msg/plugins/following.py +78 -0
- msg/plugins/hosting_capacity.py +52 -0
- msg/plugins/identity.py +1654 -0
- msg/plugins/money.py +221 -0
- msg/plugins/offers.py +183 -0
- msg/plugins/orders.py +273 -0
- msg/plugins/recovery.py +376 -0
- msg/plugins/schemas.py +5 -0
- msg/plugins/sharing.py +300 -0
- msg/plugins/store.py +249 -0
- msg/plugins/system.py +75 -0
- msg/plugins/transfer.py +249 -0
- msg/py.typed +0 -0
- msg/security/__init__.py +0 -0
- msg/security/age_keys.py +112 -0
- msg/security/authentication.py +141 -0
- msg/security/authorization.py +300 -0
- msg/security/backup_retirement.py +135 -0
- msg/security/capabilities.py +157 -0
- msg/security/certificates.py +183 -0
- msg/security/crypto.py +102 -0
- msg/security/custodial_migration.py +292 -0
- msg/security/custody_history.py +146 -0
- msg/security/network.py +81 -0
- msg/security/policy.py +67 -0
- msg/security/quarantine.py +19 -0
- msg/security/root_files.py +67 -0
- msg/security/rotation_journal.py +60 -0
- msg/security/sealed_box.py +70 -0
- msg/security/sharing_policy.py +29 -0
- msg/security/token_delivery.py +93 -0
- msg/security/vault.py +186 -0
- msg/storage/__init__.py +0 -0
- msg/storage/capacity.py +100 -0
- msg/storage/custodial_migration.py +22 -0
- msg/storage/git.py +435 -0
- msg/storage/ledger_migration.py +307 -0
- msg/storage/market_migration.py +49 -0
- msg/storage/postgres.py +663 -0
- msg/storage/query.py +39 -0
- msg/storage/read_only.py +126 -0
- msg/storage/session.py +403 -0
- msg/storage/sqlite.py +360 -0
- msg/storage/topic_event_migration.py +26 -0
- msg/storage/valkey_bus.py +45 -0
- msg/transports/__init__.py +0 -0
- msg/transports/client.py +183 -0
- msg/transports/dictionary.py +387 -0
- msg/transports/graphql.py +75 -0
- msg/transports/http.py +72 -0
- msg/transports/http_routes.py +1577 -0
- msg/transports/mcp.py +127 -0
- msg/transports/packet.py +42 -0
- msg/transports/read_tree_path.py +65 -0
- msg/transports/stdio.py +36 -0
- msg/transports/url_safety.py +123 -0
- msg/tui.py +368 -0
- msg/workers/__init__.py +1 -0
- msg/workers/effects.py +406 -0
- msg/workers/leases.py +19 -0
- msg/workers/mail.py +63 -0
- msg/workers/maintenance.py +310 -0
- msg/workers/sandbox.py +74 -0
- msg/workers/sandbox_child.py +189 -0
- msg/workers/webhook.py +160 -0
- msgctl-0.1.0a1.dist-info/METADATA +96 -0
- msgctl-0.1.0a1.dist-info/RECORD +176 -0
- msgctl-0.1.0a1.dist-info/WHEEL +4 -0
- msgctl-0.1.0a1.dist-info/entry_points.txt +4 -0
msg/tui.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Small, line-oriented terminal navigator over the public read contracts.
|
|
2
|
+
|
|
3
|
+
The UI deliberately has no mutation command. It never interprets links or
|
|
4
|
+
resource text as terminal control sequences or as operations to execute.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
import unicodedata
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from urllib.parse import urlsplit
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from msg.core.errors import Failure
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
READ_OPERATIONS = frozenset({
|
|
21
|
+
'discovery.get', 'discovery.read_query', 'discovery.lexical_search',
|
|
22
|
+
'communication.inbox', 'communication.outbox', 'communication.following', 'discussion.thread',
|
|
23
|
+
'identity.note_list', 'identity.note_get', 'identity.todo_list', 'identity.todo_get',
|
|
24
|
+
})
|
|
25
|
+
_CONTROL = re.compile(r'[\x00-\x1f\x7f-\x9f]')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def safe_text(value):
|
|
29
|
+
"""Treat all server text as data, including ANSI/OSC and bidi controls."""
|
|
30
|
+
if value is None:
|
|
31
|
+
return ''
|
|
32
|
+
if not isinstance(value, str):
|
|
33
|
+
value = str(value)
|
|
34
|
+
return ''.join(' ' if _CONTROL.fullmatch(char) or char in '\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069'
|
|
35
|
+
else char for char in value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def terminal_lines(value, width):
|
|
39
|
+
"""Preserve paragraphs and conservatively wrap wide/combining characters.
|
|
40
|
+
|
|
41
|
+
Emoji join sequences can occupy fewer cells than this conservative count;
|
|
42
|
+
we prefer an early wrap over writing beyond a narrow terminal's boundary.
|
|
43
|
+
"""
|
|
44
|
+
for paragraph in ('' if value is None else str(value)).split('\n'):
|
|
45
|
+
line = ''
|
|
46
|
+
used = 0
|
|
47
|
+
for token in re.findall(r'[!-~]+|[^!-~]', safe_text(paragraph)):
|
|
48
|
+
# Keep a URL/error code/ASCII word intact when it fits on one line.
|
|
49
|
+
if token.isascii() and 1 < len(token) <= width and used + len(token) > width:
|
|
50
|
+
yield line.rstrip()
|
|
51
|
+
line, used = '', 0
|
|
52
|
+
for char in token:
|
|
53
|
+
cells = (0 if unicodedata.combining(char) or unicodedata.category(char) == 'Cf'
|
|
54
|
+
else 2 if unicodedata.east_asian_width(char) in {'W', 'F'} else 1)
|
|
55
|
+
if cells > width:
|
|
56
|
+
char, cells = '?', 1
|
|
57
|
+
if cells and used + cells > width:
|
|
58
|
+
yield line.rstrip()
|
|
59
|
+
line, used = '', 0
|
|
60
|
+
line += char
|
|
61
|
+
used += cells
|
|
62
|
+
yield line.rstrip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
HELP = ('命令:h Home,id 身份,topics 话题,following 关注,i Inbox,o Outbox,notes 笔记,'
|
|
66
|
+
'todos 待办,files 文件,groups 组织,credentials 本地凭据,'
|
|
67
|
+
's <scope> <terms> 搜索,t <id> 线程,r <id> 读取,'
|
|
68
|
+
'n 下一页,retry 重新读取,q 退出。')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def service_origin(value):
|
|
72
|
+
"""The connection URL may be misconfigured; never display its credentials."""
|
|
73
|
+
try:
|
|
74
|
+
parsed = urlsplit(value)
|
|
75
|
+
if parsed.scheme not in {'http', 'https'} or not parsed.hostname:
|
|
76
|
+
return '(未配置)'
|
|
77
|
+
host = '[' + parsed.hostname + ']' if ':' in parsed.hostname else parsed.hostname
|
|
78
|
+
port = ':' + str(parsed.port) if parsed.port is not None else ''
|
|
79
|
+
return parsed.scheme + '://' + host + port
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
return '(无效地址)'
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TerminalUI:
|
|
85
|
+
def __init__(self, client, *, stdin=None, stdout=None, width=None):
|
|
86
|
+
self.client = client
|
|
87
|
+
self.stdin = stdin or sys.stdin
|
|
88
|
+
self.stdout = stdout or sys.stdout
|
|
89
|
+
self.width = width
|
|
90
|
+
self.page = None
|
|
91
|
+
self._last_read = None
|
|
92
|
+
self._read_context = None
|
|
93
|
+
|
|
94
|
+
def _context(self):
|
|
95
|
+
state = self.client.state
|
|
96
|
+
# Public identifiers only; do not inspect the token, signer or journals.
|
|
97
|
+
return (getattr(state, 'subject', None), getattr(state, 'server', None),
|
|
98
|
+
tuple(getattr(state, 'certificates', ())))
|
|
99
|
+
|
|
100
|
+
def _remember_read(self, function, *arguments):
|
|
101
|
+
self.page = None
|
|
102
|
+
self._last_read = (function, arguments)
|
|
103
|
+
self._read_context = self._context()
|
|
104
|
+
|
|
105
|
+
def _discard_stale_read(self):
|
|
106
|
+
if self._read_context is not None and self._read_context != self._context():
|
|
107
|
+
self.page = self._last_read = self._read_context = None
|
|
108
|
+
self._write('身份或服务已变化;请重新选择视图。')
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def _write(self, value=''):
|
|
113
|
+
columns = self.width if self.width is not None else shutil.get_terminal_size((80, 24)).columns
|
|
114
|
+
width = max(1, min(columns, 160))
|
|
115
|
+
for line in terminal_lines(value, width):
|
|
116
|
+
self.stdout.write(line + '\n')
|
|
117
|
+
self.stdout.flush()
|
|
118
|
+
|
|
119
|
+
async def _read(self, operation, arguments):
|
|
120
|
+
if operation not in READ_OPERATIONS:
|
|
121
|
+
raise ValueError('TUI only supports read operations')
|
|
122
|
+
try:
|
|
123
|
+
result = await self.client.call(operation, arguments)
|
|
124
|
+
if self._discard_stale_read():
|
|
125
|
+
return None
|
|
126
|
+
except (Failure, OSError, TimeoutError, httpx.HTTPError) as exc:
|
|
127
|
+
self.page = None
|
|
128
|
+
code = exc.code if isinstance(exc, Failure) else 'transport_unavailable'
|
|
129
|
+
self._read_error(code)
|
|
130
|
+
return None
|
|
131
|
+
if result.status != 'ok':
|
|
132
|
+
self.page = None
|
|
133
|
+
self._read_error(getattr(getattr(result, 'error', None), 'code', None))
|
|
134
|
+
return None
|
|
135
|
+
if not isinstance(result.data, Mapping):
|
|
136
|
+
return self._invalid_response()
|
|
137
|
+
return result.data
|
|
138
|
+
|
|
139
|
+
def _invalid_response(self):
|
|
140
|
+
# A server/proxy that breaks the read contract must not crash the loop
|
|
141
|
+
# or leave the previous page selectable.
|
|
142
|
+
self.page = None
|
|
143
|
+
self._read_error('invalid_read_response')
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def _read_error(self, code):
|
|
147
|
+
# Exception text/URL/details may contain credentials. Only contract codes
|
|
148
|
+
# reach the terminal; a retry always re-enters the signed read contract.
|
|
149
|
+
if not isinstance(code, str) or not re.fullmatch(r'[a-z][a-z0-9_]{0,95}', code):
|
|
150
|
+
code = 'read_failed'
|
|
151
|
+
self._write('读取失败:' + code)
|
|
152
|
+
self._write('retry 重新读取;不自动提交 ACK 或其他写入。')
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _item_id(item):
|
|
156
|
+
ref = item.get('ref') or item.get('resource')
|
|
157
|
+
for rid in (item.get('id'), ref.get('id') if isinstance(ref, Mapping) else None,
|
|
158
|
+
item.get('conversation_id')):
|
|
159
|
+
if isinstance(rid, str) and rid:
|
|
160
|
+
return rid
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def _show_items(self, items):
|
|
164
|
+
for index, item in enumerate(items, 1):
|
|
165
|
+
rid = self._item_id(item) or ''
|
|
166
|
+
name = item.get('title') or item.get('name') or item.get('path') or item.get('source') or rid
|
|
167
|
+
self._write(f'{index}. {name} {rid}')
|
|
168
|
+
snippet = item.get('snippet')
|
|
169
|
+
if isinstance(snippet, dict) and snippet.get('text'):
|
|
170
|
+
self._write(' ' + snippet['text'])
|
|
171
|
+
|
|
172
|
+
async def _show_page(self, operation, arguments, title):
|
|
173
|
+
# A cursor may become invalid after a grant changes. Never keep the
|
|
174
|
+
# previous page selectable after a failed permission recheck.
|
|
175
|
+
self._remember_read(self._show_page, operation, dict(arguments), title)
|
|
176
|
+
data = await self._read(operation, arguments)
|
|
177
|
+
if data is None:
|
|
178
|
+
return
|
|
179
|
+
items = data.get('items', [])
|
|
180
|
+
cursor = data.get('next_after_name') if operation == 'identity.todo_list' else data.get('cursor')
|
|
181
|
+
if (not isinstance(items, (list, tuple)) or not all(isinstance(item, Mapping) for item in items)
|
|
182
|
+
or not (cursor is None or isinstance(cursor, str))):
|
|
183
|
+
self._invalid_response()
|
|
184
|
+
return
|
|
185
|
+
self.page = {'operation': operation, 'arguments': arguments, 'cursor': cursor,
|
|
186
|
+
'title': title, 'items': items}
|
|
187
|
+
self._write(title)
|
|
188
|
+
if items:
|
|
189
|
+
self._show_items(items)
|
|
190
|
+
else:
|
|
191
|
+
self._write('(空)')
|
|
192
|
+
if self.page['cursor']:
|
|
193
|
+
self._write('n 下一页')
|
|
194
|
+
|
|
195
|
+
async def home(self):
|
|
196
|
+
subject = getattr(self.client.state, 'subject', None)
|
|
197
|
+
self._write('msg ' + (subject or '未登录(公开内容)'))
|
|
198
|
+
await self._show_page('discovery.read_query', {'parent': '/', 'limit': 20}, 'Home')
|
|
199
|
+
|
|
200
|
+
def identity(self):
|
|
201
|
+
state = self.client.state
|
|
202
|
+
subject = getattr(state, 'subject', None)
|
|
203
|
+
self._write('身份:' + (subject or '未登录;可以浏览公开内容'))
|
|
204
|
+
self._write('服务:' + service_origin(getattr(state, 'server', '')))
|
|
205
|
+
if subject:
|
|
206
|
+
self._write('身份管理请使用 msg identity;TUI 不接收密钥或令牌。')
|
|
207
|
+
|
|
208
|
+
async def inbox(self):
|
|
209
|
+
await self.private_page('communication.inbox', {'limit': 20}, 'Inbox')
|
|
210
|
+
|
|
211
|
+
async def private_page(self, operation, arguments, title):
|
|
212
|
+
if not getattr(self.client.state, 'subject', None):
|
|
213
|
+
self.page = self._last_read = self._read_context = None
|
|
214
|
+
self._write(title + ' 需要已登录身份。')
|
|
215
|
+
return
|
|
216
|
+
await self._show_page(operation, arguments, title)
|
|
217
|
+
|
|
218
|
+
async def files(self):
|
|
219
|
+
subject = getattr(self.client.state, 'subject', None)
|
|
220
|
+
self.page = None
|
|
221
|
+
if not subject:
|
|
222
|
+
self._last_read = self._read_context = None
|
|
223
|
+
self._write('Files 需要已登录身份。')
|
|
224
|
+
return
|
|
225
|
+
self._remember_read(self.files)
|
|
226
|
+
profile = await self._read('discovery.get', {'id': subject, 'fields': ['path']})
|
|
227
|
+
if profile is not None and not (isinstance(profile.get('path'), str)
|
|
228
|
+
and profile['path'].startswith('/')):
|
|
229
|
+
self._invalid_response()
|
|
230
|
+
elif profile is not None:
|
|
231
|
+
await self._show_page('discovery.read_query',
|
|
232
|
+
{'parent': profile['path'] + '/files', 'limit': 20}, 'Files')
|
|
233
|
+
|
|
234
|
+
def credentials(self):
|
|
235
|
+
self.identity()
|
|
236
|
+
# Never inspect or stringify ClientState.data, signer, token or journals.
|
|
237
|
+
self._write('本地证书 ID(在线有效性由每次请求重新检查):')
|
|
238
|
+
for certificate in getattr(self.client.state, 'certificates', ()):
|
|
239
|
+
self._write(certificate)
|
|
240
|
+
|
|
241
|
+
async def search(self, scope, terms):
|
|
242
|
+
if not scope or not terms:
|
|
243
|
+
self._write('用法:s <scope> <terms>')
|
|
244
|
+
return
|
|
245
|
+
await self._show_page('discovery.lexical_search',
|
|
246
|
+
{'scope': scope, 'terms': terms, 'limit': 20,
|
|
247
|
+
'snippet': True}, '搜索')
|
|
248
|
+
|
|
249
|
+
async def thread(self, rid):
|
|
250
|
+
if not rid:
|
|
251
|
+
self._write('用法:t <resource-id>')
|
|
252
|
+
return
|
|
253
|
+
await self._show_page('discussion.thread', {'id': rid, 'limit': 20}, '线程 ' + rid)
|
|
254
|
+
|
|
255
|
+
async def read(self, rid):
|
|
256
|
+
if not rid:
|
|
257
|
+
self._write('用法:r <resource-id>')
|
|
258
|
+
return
|
|
259
|
+
await self._document('discovery.get', {'id': rid}, rid)
|
|
260
|
+
|
|
261
|
+
async def _document(self, operation, arguments, title):
|
|
262
|
+
self._remember_read(self._document, operation, dict(arguments), title)
|
|
263
|
+
data = await self._read(operation, arguments)
|
|
264
|
+
if data is None:
|
|
265
|
+
return
|
|
266
|
+
self._write(data.get('path') or data.get('name') or title)
|
|
267
|
+
content = data.get('content')
|
|
268
|
+
if isinstance(content, str):
|
|
269
|
+
self._write(content)
|
|
270
|
+
elif operation == 'identity.todo_get':
|
|
271
|
+
self._write(data.get('title', ''))
|
|
272
|
+
self._write(data.get('description', ''))
|
|
273
|
+
self._write('状态:' + str(data.get('status', '')))
|
|
274
|
+
else:
|
|
275
|
+
self._write('(无文本正文)')
|
|
276
|
+
self._write('读取不会发送 ACK。')
|
|
277
|
+
|
|
278
|
+
async def next_page(self):
|
|
279
|
+
if self._discard_stale_read():
|
|
280
|
+
return
|
|
281
|
+
if not self.page or not self.page['cursor']:
|
|
282
|
+
self._write('没有下一页。')
|
|
283
|
+
return
|
|
284
|
+
current = self.page
|
|
285
|
+
arguments = ({'cursor': current['cursor']} if current['operation'] in
|
|
286
|
+
{'discovery.read_query', 'discovery.lexical_search', 'communication.following'} else
|
|
287
|
+
{**current['arguments'],
|
|
288
|
+
'after_name' if current['operation'] == 'identity.todo_list' else 'cursor': current['cursor']})
|
|
289
|
+
await self._show_page(current['operation'], arguments, current['title'])
|
|
290
|
+
|
|
291
|
+
async def command(self, raw):
|
|
292
|
+
self._discard_stale_read()
|
|
293
|
+
command = raw.strip()
|
|
294
|
+
if command in {'q', 'quit', 'exit'}:
|
|
295
|
+
return False
|
|
296
|
+
if command in {'h', 'home', ''}:
|
|
297
|
+
await self.home()
|
|
298
|
+
elif command in {'id', 'identity'}:
|
|
299
|
+
self.identity()
|
|
300
|
+
elif command in {'credentials', 'creds'}:
|
|
301
|
+
self.credentials()
|
|
302
|
+
elif command == 'topics':
|
|
303
|
+
await self._show_page('discovery.read_query', {'type': 'topic', 'limit': 20}, 'Topics')
|
|
304
|
+
elif command == 'groups':
|
|
305
|
+
await self._show_page('discovery.read_query', {'type': 'organization', 'limit': 20}, 'Groups')
|
|
306
|
+
elif command in {'o', 'outbox'}:
|
|
307
|
+
await self.private_page('communication.outbox', {'limit': 20}, 'Outbox')
|
|
308
|
+
elif command == 'following':
|
|
309
|
+
await self.private_page('communication.following', {'limit': 20}, 'Following')
|
|
310
|
+
elif command == 'notes':
|
|
311
|
+
await self.private_page('identity.note_list', {}, 'Notes')
|
|
312
|
+
elif command == 'todos':
|
|
313
|
+
await self.private_page('identity.todo_list', {'limit': 20}, 'Todos')
|
|
314
|
+
elif command == 'files':
|
|
315
|
+
await self.files()
|
|
316
|
+
elif command == 'retry':
|
|
317
|
+
if self._last_read is None:
|
|
318
|
+
self._write('没有可重新读取的请求。')
|
|
319
|
+
else:
|
|
320
|
+
function, arguments = self._last_read
|
|
321
|
+
await function(*arguments)
|
|
322
|
+
elif command in {'i', 'inbox'}:
|
|
323
|
+
await self.inbox()
|
|
324
|
+
elif command in {'n', 'next'}:
|
|
325
|
+
await self.next_page()
|
|
326
|
+
elif command.startswith('s '):
|
|
327
|
+
parts = command.split(maxsplit=2)
|
|
328
|
+
await self.search(parts[1] if len(parts) > 1 else '',
|
|
329
|
+
parts[2] if len(parts) > 2 else '')
|
|
330
|
+
elif command.startswith('t '):
|
|
331
|
+
await self.thread(command[2:].strip())
|
|
332
|
+
elif command.startswith('r '):
|
|
333
|
+
await self.read(command[2:].strip())
|
|
334
|
+
elif command.isdecimal() and self.page:
|
|
335
|
+
index = int(command) - 1 if len(command) <= 6 else -1
|
|
336
|
+
if 0 <= index < len(self.page['items']):
|
|
337
|
+
item = self.page['items'][index]
|
|
338
|
+
if self.page['operation'] in {'identity.note_list', 'identity.todo_list'}:
|
|
339
|
+
name = item.get('name')
|
|
340
|
+
if isinstance(name, str) and name:
|
|
341
|
+
operation = self.page['operation'].replace('_list', '_get')
|
|
342
|
+
await self._document(operation, {'name': name}, name)
|
|
343
|
+
else:
|
|
344
|
+
self._write('该条目没有可读取的标识。')
|
|
345
|
+
elif rid := self._item_id(item):
|
|
346
|
+
await self.read(rid)
|
|
347
|
+
else:
|
|
348
|
+
self._write('该条目没有可读取的标识。')
|
|
349
|
+
else:
|
|
350
|
+
self._write('序号不在本页。')
|
|
351
|
+
else:
|
|
352
|
+
self._write(HELP)
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
async def run(self):
|
|
356
|
+
self._write(HELP)
|
|
357
|
+
await self.home()
|
|
358
|
+
while True:
|
|
359
|
+
self.stdout.write('msg> ')
|
|
360
|
+
self.stdout.flush()
|
|
361
|
+
line = self.stdin.readline()
|
|
362
|
+
if not line or not await self.command(line):
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def run_tui(client, *, stdin=None, stdout=None, width=None):
|
|
367
|
+
"""Run the read-only UI; the owner of ``client`` closes its transport."""
|
|
368
|
+
await TerminalUI(client, stdin=stdin, stdout=stdout, width=width).run()
|
msg/workers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Trusted external effect workers."""
|