
    _je7                       d Z ddlmZ ddlZddlZddlZddlZddlZddlZ	ddl
Z	ddlZddlmZmZ ddlmZ  ee      j#                         j$                  j$                  Zedz  dz  dz  Zed	z  j+                         r!ed	z  j-                         j/                         ng D ]t  Zd
ev sej3                         j5                  d      r(ej7                  d
      \  ZZZej>                  jA                  ej3                         ej3                                v ej>                  jC                  dd      Z"ej>                  jC                  dd      Z#de" Z$ej>                  jC                  dd      jK                  d      D  ch c]"  } | j3                         r| j3                         $ c} a&ej>                  jC                  dd      j3                         Z'ej>                  jC                  dd      j3                         Z(ed	z  Z)ejT                  jW                  d e,edz               ddl-m.Z.m/Z/ d0dZ0d0dZ1d1dZ2d2dZ3d3dZ4dZ5dZ6dZ7dZ8d4d Z9d5d!Z:d6d"Z;d7d#Z<edz  dz  d$z  Z=d8d%Z>d9d&Z?	 	 d:	 	 	 d;d'Z@d<d(ZAd=d)ZBd>d*ZC	 d?	 	 	 d@d+ZDdAd,ZEdAd-ZFeGd.k(  r- ej                  d/ej                  v r eF       n eE              yyc c} w )Bu  Telegram intake bot: transport only, the agent loop is the brains.

One chat message = one ring request:
  customer text -> artifacts/review/requests.json (status: "new")
  bot replies "designing…" immediately.
The agent loop (Claude on the server) turns each new request into a RingSpec,
builds it, and flips the entry to status "done" (or "failed"). This bot
watches for that and sends the studio link:
  {STUDIO_BASE_URL}/viewer/design.html?name=<name>

Run:  .cad-venv/bin/python harness/telegram_bot.py          (long-poll loop)
      .cad-venv/bin/python harness/telegram_bot.py --selftest
Config (.env): TELEGRAM_BOT_TOKEN=...   STUDIO_BASE_URL=http://127.0.0.1:8770
No third-party deps: urllib against api.telegram.org.
    )annotationsN)datetimetimezone)Path	artifactsreviewzrequests.jsonz.env=#TELEGRAM_BOT_TOKEN STUDIO_BASE_URLzhttp://127.0.0.1:8770zhttps://api.telegram.org/botTELEGRAM_ALLOWED_CHATS,TELEGRAM_OWNER_CHATTELEGRAM_BOT_USERNAMEharness)
make_tokentoken_okc                     t        d      S )zOne shared, unforgeable invite token (HMAC under STUDIO_SECRET). Anyone
    who opens the deep link and taps Start presents it and is allowlisted.
__invite__)r        &/opt/ringagent/harness/telegram_bot.pyinvite_tokenr   6   s     l##r   c                 @    t               } t        r| sydt         d|  S )Nr   zhttps://t.me/z?start=invite_)r   BOT_USERNAME)toks    r   invite_linkr   <   s$    
.Cs<.se<<r   c                 z   ddj                  t        t                    z   } t        j	                         syt        j                         j                         }t        |      D ]  \  }}|j                  d      s| ||<    n |j                  |        t        j                  dj                  |      dz          y)zzRewrite TELEGRAM_ALLOWED_CHATS in .env from the in-memory set, so a bot
    restart keeps everyone who accepted an invite.zTELEGRAM_ALLOWED_CHATS=r   N
)joinsortedALLOWEDENV_FILEexists	read_text
splitlines	enumerate
startswithappend
write_text)linelinesils       r   _persist_allowedr0   C   s     %sxxw'@@D?? ++-E%  1<<12E!H
 	T		%(4/0r   c                T    t         j                  t        |              t                y N)r#   addstrr0   chat_ids    r   
allow_chatr7   S   s    KKGr   c                ~    t        |       }t         dt        j                  j	                  |        }|r| d| S |S )Nz/viewer/design.html?name=z&token=)r   BASEurllibparsequote)designr   links      r   studio_linkr?   X   sE    
V
CV,V\\-?-?-G,HID$'dV73% 1T1r   zUThis is a private studio. Ask the owner for access and give them this code: {chat_id}u   💍 Tell me about the ring you want: cut, carat, metal, size, style: or send a reference description. One message per design.
Commands: /status: where your latest design is at.u   🛠 Designing your ring: I'll send a link here in a few minutes where you can spin it in 3D, leave notes, and approve it for printing.i   c                     t         j                         r't        j                  t         j	                               S dg iS )Nrequests)REQUESTSr%   jsonloadsr&   r   r   r   _loadrE   i   s/    zz(,,.//r   c                    t         j                  j                  dd       t         j                  t	        j
                  | d             y )NTparentsexist_ok   )indent)rB   parentmkdirr+   rC   dumpsdatas    r   _saverQ   o   s2    OO$6

423r   c                   t        j                  d| j                               d d }dj                  |t	        j
                         j                  d d gz         xs$ dt	        j
                         j                  d d  S )Nz[a-z]+   -   zring-   )refindalllowerr!   uuiduuid4hex)textwordss     r   _slugr_   t   sk    JJy$**,/3E88ETZZ\--bq1223Utzz|?O?OPRQR?S>T7UUr   c                `   t         j                  j                  t         d|  t         j                  j                  |      j                               }t         j                  j                  |d      5 }t        j                  |j                               cd d d        S # 1 sw Y   y xY w)N/rO   F   timeout)r:   requestRequestAPIr;   	urlencodeencodeurlopenrC   rD   read)methodparamsreqrs       r   _tgrp   y   s    
..
 
 C5&!2&,ll&<&<V&D&K&K&M ! OC			R		0 $Azz!&&(#$ $ $s   7#B$$B-uploadsc                D   	 t        d|       }|j                  d      xs i j                  d      }|syt        |      j                  xs d}t        j                  dd       t        | | z  }d	t         d
| }t        j                  j                  |d      5 }t        |d      5 }|j                  |j                                ddd       ddd       t        |      S # 1 sw Y   xY w# 1 sw Y    xY w# t        $ r}	t        d|	       Y d}	~	yd}	~	ww xY w)zFetch a Telegram photo to a local path so the design agent can Read it.
    Returns the saved path, or None on any failure (caller falls back to text).getFile)file_idresult	file_pathNz.jpgTrG   z!https://api.telegram.org/file/botra   rb   rc   wbzphoto download failed:)rp   getr   suffixUPLOADSrM   TOKENr:   re   rj   openwriterk   r4   	Exceptionprint)
rt   ridinfofpathextdesturlro   fes
             r   download_photor      s   9g.(#)r..{;5k  *FdT2C5&1%%A^^##C#4 	4d;K 	qGGAFFH	 	4y	 	 	 	  &*sM   3D  A*D   C4- C(C4D  (C1	-C44C=9D   	D	DDc                x    t               d   D cg c]  }|d   | k(  r
|d   dk(  r| }}|r|d   d   S dS c c}w )u   The customer's most recent finished design name — used to route a
    follow-up message as a revision rather than a brand-new ring.rA   r6   statusdonenameN)rE   )r6   ro   mines      r   latest_done_designr      s]     wz* B!|w&1X;&+@  BD B#48F--Bs   7c           	     B   t               }t        j                         j                  d d | |t	        |xs d      ddt        j                  t        j                        j                  d      d}|r||d<   |r||d	<   |d
   j                  |       t        |       |S )N   z
photo ringnewFseconds)timespec)idr6   r]   r   r   notifiedcreatedimagereply_torA   )rE   rZ   r[   r\   r_   r   nowr   utc	isoformatr*   rQ   )r6   r]   r   r   rP   entrys         r   enqueuer      s    7D::<##BQ'GT4/<0E X\\2<<i<PRE g$jE"	$KLr   c                 r    t               d   D  cg c]  } | d   dv r| j                  d      s|  c} S c c} w )NrA   r   )r   failedr   )rE   rx   )ro   s    r   pending_notificationsr      sD    wz* L!{00z9J  L L Ls   !4c                ^    t               }|d   D ]  }|d   | k(  sd|d<    t        |       y )NrA   r   Tr   )rE   rQ   )r   rP   ro   s      r   mark_notifiedr      s:    7D* !T7c> AjM! 
$Kr   c                    | d   dk(  r*| j                  d      rdnd}dt        | d          d|z   S d	| j                  d
      xs dz   S )Nr   r   size_assumeduB   

📏 I assumed size 6.5 — reply with your ring size to adjust.r   u   ✨ Your ring is ready!

r   zr

Spin it in 3D, leave notes if you want changes, and hit Approve when you love it: that prepares the print files.u    😔 I couldn't build that one: errorzBthe request needs more detail (cut, carat, metal). Try rephrasing?)rx   r?   )ro   	size_notes     r   notify_textr      s|    {f"#%%"7=? 	-qy)* +KK 	 	
 /!%%. 3E EE F Gr   c                   |xs dj                         }| |s|sg S |j                  d      rK|j                  dd      d   j                         }t        d|      rt	        |        | dt
        z   fgS | dfgS t        r*t        |       t        vr| t        j                  | 	      fgS |j                  d
      r6t        rt        |       t        k7  r| dfgS t               }|s| dfgS | d|z   fgS |j                  d      r	| t
        fgS |j                  d      rwt               d   D cg c]  }|d   | k(  s| }}|s| dfgS |d   }ddddj                  |d   |d         }|d   dk(  rdt        |d          nd}| d|d    d| d| fgS t        |      t         kD  r| dt          d fgS |rdn
t#        |       }t%        | |||!       |r| d"fgS | t&        fgS c c}w )#zPure handler: returns [(chat_id, reply_text), ...] and mutates only the
    queue. Testable without network. `image` is an already-downloaded local
    path (poll_loop does the network fetch); text may be a photo caption.r   Nz/start invite_invite_   r   u   ✅ You're in! zKThat invite link isn't valid or has changed. Ask the owner for a fresh one.r5   z/invitez)Only the studio owner can create invites.zLInvite links aren't configured (need STUDIO_SECRET + TELEGRAM_BOT_USERNAME).uS   🔗 Share this link to invite someone — they tap Start and get instant access:

/start/statusrA   r6   z)No designs yet: describe a ring to start!r   u   🛠 being designedu	   ✨ readyu   😔 needs a rephrase)r   r   r   r   r   r    r   zYour latest design 'z' is .zThat's a lot! Keep it under z- characters - cut, carat, metal, size, style.)r   r   uM   🛠 Designing from your photo — I'll send a 3D link here in a few minutes.)stripr)   splitr   r7   WELCOMEr#   r4   DENIEDformatOWNERr   rE   rx   r?   lenMAX_TEXTr   r   ACK)	r6   r]   r   r   r>   ro   r   stager   s	            r   handle_messager      sy   
 JBDtE	 '(jjA&q)//1L#&w/'9:;; 7 8 9 	93w<w.&---89::y!S\U*IJKK} N O P P >@DE F G 	Gx '"##y! 7:.Ja!I,'2IJJIJKKH-{2447C(Qx[4Q 	01(v0EK&	*+,2065qOPQQ
4y88
 C> > ? @ 	@ t"4W"=HGT: 3 4 5 	5cN% Ks   *G8Gc            
        t         st        d       yt        dt                d} 	 t               D ]I  }	 t	        d|d   t        |             t        |d          t        d	|d    d
|d    d|d    d       K 	 t	        dd|       }|j                  dg       D ]  }|d   dz   } |j                  d      xs i }|j                  d      xs i j                  d      }d }|j                  d      }|j                  d      }	|	r[|Yt        rt        |      t        v r1t        |	d   d   t        j                         j                   d d       }|j                  d      }t#        |||      D ]  \  }
}t	        d|
|         d# t        $ r}t        d|       Y d }~ud }~ww xY w# t        $ r,}t        d|       t        j                  d       Y d }~d }~ww xY w) Nz`TELEGRAM_BOT_TOKEN not set in .env: add it, then rerun.
Get a token from @BotFather on Telegram.r   zbot up: studio base r   sendMessager6   )r6   r]   r   z	notified z about r   z (r   )znotify failed:
getUpdates2   )rd   offsetzpoll error:   ru   	update_idmessagechatr]   photor   rt   r   captionr   )r{   r   r9   r   rp   r   r   r~   timesleeprx   r#   r4   r   rZ   r[   r\   r   )r   ro   r   updumsgcid_inr   r]   r   cidreplys               r   	poll_loopr      s    9 	:	 
'(F
&( 	+A+M1Y<k!nMag&	!I,wqykAhK=PQRS		+	lBv>C
 2& 	<A{^a'F%%	"(bCggfo+006FE776?DGGG$E+#f+"8*59Y+?+/::<+;+;BQ+?AEwwy),VTG <
UM3U;<	<!   +&**+
  	-#JJqM	s0   AF?F/ 	F,F''F,/	G$8!GG$c                    h da t        dd      } d| d   d   v r
d| d   d   v sJ |        t        dd      d   d   j                  d	      sJ d
t        dd      d   d   v sJ dt        dd      d   d   v sJ t        dd      }|d   d   t        k(  sJ t	               d   D cg c]  }|d   dk(  s| c}d   t        dd      d   d   }d|v sJ |       t        fdt	               d   D              sJ t	               }|d   D ]  }|d   d   k(  sd|d<    t        |       t               }t        fd|D              sJ d       t        |D cg c]  }|d   d   k(  s| c}d         }d|v sJ t        d          t        fdt               D              rJ dd   dd}dt        |      v sJ t        |             t        dd d!"      }d#|d   d   j                         v sJ |       t	               d   D cg c]  }|d   dk(  s| c}d   }	|	j                  d$      d!k(  rd%|	vsJ |	       t        dd&      }
t	               d   D cg c]  }|d   dk(  s| c}d   }|j                  d%      d   k(  sJ |       t	               }|d   D cg c]  }|d   dk7  s| c}|d<   t        |       t        d'|z          yc c}w c c}w c c}w c c}w c c}w )(z9No network: exercise handler + queue contract end to end.>   01987654iiz  hellozprivate studior   r   31337r   u   💍zNo designs yeti r   z	under 800  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxz,oval 1.5 carat rose gold twisted band size 6rA   r6   r   zbeing designedc              3  H   K   | ]  }|d    d    k(  xr |d   dk(    yw)r   r   r   Nr   .0ro   r   s     r   	<genexpr>zselftest.<locals>.<genexpr>4  s7      - w!D'!:akU&:: -s   "r   r   r   c              3  4   K   | ]  }|d    d    k(    ywr   Nr   r   s     r   r   zselftest.<locals>.<genexpr>=  s     0aqw!D'!0   zdone entry must need notifyzdesign.html?name=c              3  4   K   | ]  }|d    d    k(    ywr   r   r   s     r   r   zselftest.<locals>.<genexpr>A  s     G!1T7ag%Gr   r   T)r   r   r   zassumed size 6.5zthis but rose goldz/tmp/ref.jpgr   r   r   r   zmake the band thinnerzRselftest ok: queue contract + image/revision routing sound.
ready reply would be:
)r#   r   r)   r   rE   anyrQ   r   r   r   rY   rx   r   )deniedrepliesro   strP   todor   fakeiriefrfer   s               @r   selftestr   &  sY    #GE7+Fvay|+6!9Q<0GOOG!X&q)!,77???~fi@CAFFFF.I6q9!<<<<Q NOG1:a=CGJ'=q1Y<1+<=bAA	9	%a	(	+Br!%2%! -
+- - - - 7D* !T7ag AhK! 
$K "D0400O2OO0
$=Q!D'QtW*<q=a@
AC#%%%!D'G/D/FGGGGaiFDT!22EK4EE2	/~	FBbeAhnn&&**&WZ(	>AiLA,=!	>r	BB66'?n,21EIrIE	2	3BWZ(	>AiLA,=!	>r	BB66*6*.B.*7D#'
#3Iaq|q7HID	$K	 $&)* +G 	> > 
? 
? Js<   (K6KK-KK$K$!K)/K)(K.6K.__main__z
--selftest)returnr4   )r   None)r6   intr   r   )r=   r4   r   r4   )r   dict)rP   r   r   r   )r]   r4   r   r4   )rl   r4   )rt   r4   r   r4   r   
str | None)r6   r   r   r   )NN)
r6   r   r]   r4   r   r   r   r   r   r   )r   z
list[dict])r   r4   r   r   )ro   r   r   r4   r2   )r6   r   r]   r4   r   r   r   zlist[tuple[int, str]])r   r   )J__doc__
__future__r   rC   osrW   sysr   urllib.parser:   urllib.requestrZ   r   r   pathlibr   __file__resolverL   ROOTrB   r%   r&   r'   _liner   r)   	partitionk_venviron
setdefaultrx   r{   r9   rg   r   r#   r   r   r$   pathinsertr4   authtokr   r   r   r   r0   r7   r?   r   r   r   r   rE   rQ   r_   rp   rz   r   r   r   r   r   r   r   r   r   __name__exitargv)cs   0r   <module>r     s    #  	 	 
     ' H~&&--+(?::>-9O9O9QdVm&&(335WY 4E
e|EKKM44S9//#&1a


aggi34
 	

+R0	zz~~')@A$UG, ::>>2B7==cBQFGggi 779 Q 	

,b1779zz~~5r:@@B&= 3ti'( ) ($=1 
2!@J4
V
$ 
x
')
3(. :>#' ,0 L

G (,1$10E1h&<R.b zCHH<3883XZE Y	Qs   'K