1010
1111from collections .abc import Mapping
1212from datetime import datetime , timezone
13- from typing import Any , Dict , FrozenSet , Optional , Tuple , Union
13+ from typing import Any , Dict , FrozenSet , List , Optional , Tuple , Union
14+ from urllib .parse import urlsplit
1415
1516# If you change these values, make sure to update the JavaScript client code.
1617#
@@ -26,6 +27,9 @@ class RequestTags:
2627 INVALID_REQUEST = 'invalid'
2728 OUTDATED_PROTOCOL = 'outdated-proto'
2829 ROLE_OCCUPIED = 'role-occupied'
30+ # A reviewer asked a provider for a file it may not read: provider ->
31+ # server (naming the reviewer), which closes that reviewer's connection.
32+ ILLEGAL_ACCESS = 'illegal-access'
2933 META_SECTION_NAME = 'meta.section.name'
3034 META_REVIEW_NAME = 'meta.review.name'
3135 META_REVIEW_TIMESTAMP = 'meta.review.timestamp'
@@ -117,7 +121,7 @@ def request_tags_with_prefix(prefix: str) -> FrozenSet[str]:
117121 ALLOWED_BENCH_EDIT_TAGS
118122)
119123
120- PROTOCOL_REVISION = 18
124+ PROTOCOL_REVISION = 19
121125
122126# 10MiB
123127MAX_MESSAGE_SIZE = 10485760
@@ -140,6 +144,72 @@ def review_display_name(name: Optional[str] = None) -> str:
140144 from edgdocker import render_user_name
141145 return f"Review opened by { render_user_name ()} "
142146
147+ # A review's name is rich text: segments composed in order, each either
148+ # {'type': 'text', 'text': ...} or {'type': 'link', 'url': ..., 'text': ...}
149+ # (a link's text is optional; its URL is shown without it). See PROTOCOL.md.
150+ RichName = List [Dict [str , str ]]
151+
152+ MAX_RICH_NAME_SEGMENTS = 32
153+ # Characters, across every segment's text and URL.
154+ MAX_RICH_NAME_LENGTH = 4096
155+
156+ _RICH_SEGMENT_FIELDS = {
157+ 'text' : ({'type' , 'text' }, {'type' , 'text' }),
158+ 'link' : ({'type' , 'url' }, {'type' , 'url' , 'text' }),
159+ }
160+
161+ def rich_text (text : str ) -> Dict [str , str ]:
162+ return {'type' : 'text' , 'text' : text }
163+
164+ def rich_link (url : str , text : Optional [str ] = None ) -> Dict [str , str ]:
165+ segment = {'type' : 'link' , 'url' : url }
166+ if text is not None :
167+ segment ['text' ] = text
168+ return segment
169+
170+ def _is_web_url (url : str ) -> bool :
171+ try :
172+ parts = urlsplit (url )
173+ except ValueError :
174+ return False
175+ return parts .scheme in ('http' , 'https' ) and parts .netloc != ''
176+
177+ def is_valid_rich_name (value : Any ) -> bool :
178+ '''Return whether ``value`` is a well-formed rich name: a non-empty list
179+ of known segments with only their fields, all non-empty strings, and
180+ links only to http(s) URLs, within the size limits.
181+ '''
182+ if not isinstance (value , list ):
183+ return False
184+ if len (value ) == 0 or len (value ) > MAX_RICH_NAME_SEGMENTS :
185+ return False
186+ length = 0
187+ for segment in value :
188+ if not isinstance (segment , dict ):
189+ return False
190+ segment_type = segment .get ('type' )
191+ if segment_type not in _RICH_SEGMENT_FIELDS :
192+ return False
193+ required , allowed = _RICH_SEGMENT_FIELDS [segment_type ]
194+ keys = set (segment )
195+ if not (required <= keys <= allowed ):
196+ return False
197+ for key , field in segment .items ():
198+ if not isinstance (field , str ) or field == '' :
199+ return False
200+ if key != 'type' :
201+ length += len (field )
202+ if segment ['type' ] == 'link' and not _is_web_url (segment ['url' ]):
203+ return False
204+ return length <= MAX_RICH_NAME_LENGTH
205+
206+ def rich_name_text (name : RichName ) -> str :
207+ '''Return ``name`` as plain text (e.g. for terminal output).'''
208+ return '' .join (
209+ segment ['text' ] if segment .get ('text' ) else segment .get ('url' , '' )
210+ for segment in name
211+ )
212+
143213def utc_now_iso () -> str :
144214 '''Return the current UTC time as an ISO-8601 timestamp.'''
145215 return datetime .now (timezone .utc ).isoformat ()
0 commit comments