#!/usr/bin/env python3.14# -*- coding: utf-8 -*-"""The message spool at `<state>/messages.json` — messages and read-state.One record per **message**, never per recipient. A broadcast is therefore a singlerecord naming N recipients rather than N copies: "everyone" costs one write,read-state stays per-person, and a collaborator provisioned after a notice was sent issimply not in it — which is the correct behaviour, not an omission.**Every message stands on its own — there are no replies.** This is not an email or chatreplacement: it is how a *command* tells somebody something they must act on, and the actis always on one message. Threading bought conversation nobody was having, and cost asecond place for content to hide: an answer buried in a reply is invisible to anythingreading the message it answers.Every party to a thread appears in `recipients`, keyed by **box**(:func:`euler_webshell.msg.identity.box_of`), carrying that box's read timestamp:- the **author** is present and already read — they wrote it;- an **inbound** message (user → staff) additionally names the staff boxes at submit time, and :meth:`MessageStore.threads_for` shows *every* inbound message to *any* staff reader regardless, adding them lazily when they read. So a maintainer promoted after the fact still sees the queue, rather than only what was addressed to the roster as it stood.Bounded like the invite-request queue it resembles: a global cap, a per-author standingcap, field length caps, and a TTL swept on every access, so an unworked spool cannotgrow without limit. Bodies are stored as **plaintext** — sanitised on the way in andrendered through Jinja autoescape — so no markup ever survives to be interpreted.Single-writer by construction: one `euler-msg` process owns the file."""from__future__importannotations__all__=['BODY_MAX','MAX_PER_AUTHOR','MAX_THREADS','MessageStore','SUBJECT_MAX','THREAD_TTL_SECONDS','Thread']importsecretsimporttimefromdatetimeimportdatetime,timezonefrompathlibimportPathfromtypingimportAny,Literal,NamedTuplefromeuler_site.storeimportload_json,sanitize,save_json#: Threads the spool holds before further *new* ones are refused.MAX_THREADS:int=2000#: Live threads one author may hold open at once — bounds a single chatty sender#: without the rate limiter's burst-only view.MAX_PER_AUTHOR:int=50#: A thread lives this long after its last activity, then sweeps. Long enough that an#: answered question stays readable for a season, short enough to bound the file.THREAD_TTL_SECONDS:int=90*24*3600#: Length caps for the free-text fields (characters, after sanitising).SUBJECT_MAX:int=200BODY_MAX:int=8000#: What a thread is: a question to staff, or a notice from them.Kind=Literal['inbound','notice']def_now_iso()->str:"""Current UTC time in ISO-8601 (the store's display timestamp)."""returndatetime.now(timezone.utc).isoformat(timespec='seconds')classThread(NamedTuple):"""One thread as its readers see it (already sanitised at rest)."""id:strkind:strauthor:str# box key of whoever started itsubject:strbody:strrecipients:list[str]# box keys party to the threadcreated:strupdated:strunread:bool# for the box that askeddefsummary(self,*,author_name:str='')->dict[str,Any]:"""A view for a reader. *author_name* carries the display identity. The store holds box keys only — it never needs an e-mail address to route a message — so the caller resolves the name through the policy view and passes it in. An unresolvable box degrades to itself rather than dropping the message. """return{'id':self.id,'kind':self.kind,'author':self.author,'author_name':author_nameorself.author,'subject':self.subject,'body':self.body,'recipients':self.recipients,'created':self.created,'updated':self.updated,'unread':self.unread}classMessageStore:"""Single-writer store over the message spool, keyed by thread id."""def__init__(self,path:Path)->None:self._path=path# ── persistence ────────────────────────────────────────────────────────────────def_load(self)->dict[str,dict[str,Any]]:"""Live threads only — expired ones (TTL) are swept on every access."""now=time.time()records=load_json(self._path)return{key:recforkey,recinrecords.items()ifisinstance(rec,dict)andfloat(rec.get('expiry',0))>now}def_save(self,records:dict[str,dict[str,Any]])->None:save_json(self._path,records)# ── reading ────────────────────────────────────────────────────────────────────@staticmethoddef_visible_to(raw:dict[str,Any],box:str,*,staff:bool)->bool:"""Whether *box* may see this thread. Three ways in: it is addressed to them, they wrote it, or it is an inbound thread and they are staff (the queue is shared — see the module docstring). """ifboxin(raw.get('recipients')or{})orraw.get('author')==box:returnTruereturnstaffandraw.get('kind')=='inbound'@staticmethoddef_unread_for(raw:dict[str,Any],box:str)->bool:"""Whether *box* has yet to read this thread's latest activity. A box absent from `recipients` has never read it — which is exactly the newly-promoted maintainer looking at an older queue thread. """recipients:dict[str,Any]=raw.get('recipients')or{}entry=recipients.get(box)ifentryisNone:returnTruereturnnot(isinstance(entry,dict)andentry.get('read'))def_to_thread(self,raw:dict[str,Any],box:str)->Thread:returnThread(id=str(raw.get('id','')),kind=str(raw.get('kind','inbound')),author=str(raw.get('author','')),subject=str(raw.get('subject','')),body=str(raw.get('body','')),recipients=sorted(raw.get('recipients')or{}),created=str(raw.get('created','')),updated=str(raw.get('updated','')),unread=self._unread_for(raw,box))defthreads_for(self,box:str,*,staff:bool=False)->list[Thread]:"""Every thread *box* may read, newest activity first."""records=self._load()visible=[rawforrawinrecords.values()ifself._visible_to(raw,box,staff=staff)]returnsorted((self._to_thread(raw,box)forrawinvisible),key=lambdathread:thread.updated,reverse=True)defthread(self,thread_id:str,box:str,*,staff:bool=False)->Thread|None:"""One thread by id, or None when absent or not *box*'s to read."""raw=self._load().get(thread_id)ifrawisNoneornotself._visible_to(raw,box,staff=staff):returnNonereturnself._to_thread(raw,box)defunread_count(self,box:str,*,staff:bool=False)->int:"""How many visible threads *box* has not read — the header badge's number."""returnsum(1forthreadinself.threads_for(box,staff=staff)ifthread.unread)definbound(self,box:str='')->list[Thread]:"""Every inbound thread, oldest first — the staff queue, as a work list. *box* is whoever is looking, so each thread's `unread` is that reader's own state. Without it every row would read as unread (an empty box is in no thread's recipients), which is exactly the marker being useless. """records=self._load()threads=[self._to_thread(raw,box)forrawinrecords.values()ifraw.get('kind')=='inbound']returnsorted(threads,key=lambdathread:thread.created)# ── writing ────────────────────────────────────────────────────────────────────def_create(self,kind:Kind,author:str,subject:str,body:str,recipients:list[str])->str|None:"""Write a new thread; return its id, or None if a cap or a bad field refuses it. The author is a recipient of their own thread, already read: it is the read-state a reply resets, which is how an answer reaches the person who asked. """clean_subject=sanitize(subject,SUBJECT_MAX)clean_body=sanitize(body,BODY_MAX,allow_newlines=True)ifnotclean_subjectornotclean_bodyornotrecipients:returnNonerecords=self._load()iflen(records)>=MAX_THREADS:returnNonemine=sum(1forrawinrecords.values()ifraw.get('author')==author)ifmine>=MAX_PER_AUTHOR:returnNonenow,iso=time.time(),_now_iso()thread_id=secrets.token_hex(8)entries:dict[str,dict[str,str|None]]={author:{'read':iso}}forboxinrecipients:entries.setdefault(box,{'read':None})records[thread_id]={'id':thread_id,'kind':kind,'author':author,'subject':clean_subject,'body':clean_body,'recipients':entries,'created':iso,'updated':iso,'expiry':now+THREAD_TTL_SECONDS}self._save(records)returnthread_iddefsubmit(self,author:str,subject:str,body:str,staff:list[str])->str|None:"""Queue a user→staff question; return the thread id or None (cap / bad field)."""returnself._create('inbound',author,subject,body,staff)defnotice(self,author:str,subject:str,body:str,recipients:list[str])->str|None:"""Send a staff notice to *recipients*; return the thread id or None."""returnself._create('notice',author,subject,body,recipients)defmark_read(self,thread_id:str,box:str,*,staff:bool=False)->bool:"""Mark *thread_id* read by *box*, joining the thread if they were not in it."""records=self._load()raw=records.get(thread_id)ifrawisNoneornotself._visible_to(raw,box,staff=staff):returnFalserecipients:dict[str,Any]=raw.get('recipients')or{}recipients[box]={'read':_now_iso()}raw['recipients']=recipientsself._save(records)# `updated` is activity, not attentionreturnTruedefdrop(self,thread_id:str)->bool:"""Delete a thread outright (the staff queue's dismiss); True if one was there."""records=self._load()ifrecords.pop(thread_id,None)isNone:returnFalseself._save(records)returnTrue