From 6bd19ab6fd7c6f93fefe9022aa56fc46d15cc43f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 19 Jun 2026 09:26:51 +0100 Subject: [PATCH 01/18] website: add whitepaper working draft --- website/.eleventy.js | 1 + website/src/credits/whitepaper.pdf | Bin 0 -> 321343 bytes 2 files changed, 1 insertion(+) create mode 100644 website/src/credits/whitepaper.pdf diff --git a/website/.eleventy.js b/website/.eleventy.js index 0567dd45d8..122e5bb673 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -326,6 +326,7 @@ module.exports = function (ty) { ty.addPassthroughCopy("src/CNAME") ty.addPassthroughCopy("src/.well-known") ty.addPassthroughCopy("src/file-assets") + ty.addPassthroughCopy("src/credits") ty.addCollection('blogs', function (collection) { return collection.getFilteredByGlob('src/blog/*.md').reverse() diff --git a/website/src/credits/whitepaper.pdf b/website/src/credits/whitepaper.pdf new file mode 100644 index 0000000000000000000000000000000000000000..faf4a42aab4dac9328cee6a2d8a6f684e178c96d GIT binary patch literal 321343 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 z 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

B)G7By5$)q%ZIoGF4lVANS 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 z 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%0D2mbg>2 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΄LI57fnn4FY=$9@{2->7nKHbkSFuJpUquWE-8EPTI8H*H(B3 zW$=mTHAG=;;Rk6u1X8aWv-d zbFLxxos@%3oZU?!7i&V04-ik(%&1KH$>Er?~!Y1 zUW|QQztQ6SB5@I^s`~DF?*4%PJhZ^$aM?c}(a|}iwc11RafR@7+XS4KMED5PW?yXo zr=Vf}e+p<;OBKsH`1M+2@c9U^pDz}7L~Q;IIaB*5Ih+6Y9|O z`@Bx6(=2VPCsxI>foIXkz*A;szr&OZ?4|mJ^~P8p=h>x}i}fYPloWfe$!JMqgtcxg zap?fr9>y!}ea_);Ns0#93dadNoegy17bFXgkrigkb?x|SY6md>+SZc5(Vw0w5bnsT z?eA=G`_*R7TG55S6-F6>kF8rSfWp|x;yK${_cGZFh*%HKb2vddT&kpUmT#+*&+iSt z$=Tp~!qvU)U*t^cpX97F?SDHtTmQdH&OUqZ#y2rv`rqL5rw9CiaF2zmCZf58I`BEc zs0N6OL$Bab6Q-}$>FkJf59wwr8 zrG8n6JR({C4y|p9=?>eR370K$GY7%}+^lB%_IZQ=t?^~8xc@+yJPS)mO>QcW<`)jn zgRA_2Wu~HQ8tJ(8P5!_lr|A>349BhO3}Y54&J*X+(t#K$w~QNr!+qhFQGenzJ-f*V z9Ljj%JnB6VOX8NncyJx@pHv|6%&c9$n^>dWc$>Lm*+T8$cAIaxICDSDRDYP-3Ncr= z_l$^IM=Dzz;@a8a0T-S$yhI2JEOPx*%=}50|KF0({J$DAPX_K*_X4xhY{k~&)lKo% zMk&-alf?UIuPkpRM=O6*Dyob8w-JLvPJmR7P9?G!S+6?(GrjXcE`7`DJlYJB`w<M3uICs59s5*V{M>d#yW-m1 z%@Fw;XjJ8Kpj|{&K_?-Y&MSCXa#>wC~myt??wM2Rz#w*$bTvVtV=4+pO5`j1h_qGp&TcAYvBTOc2?8Kj4H&)j3|+0p17AeZ7?5= zz*zEZomi0K;#9qEB}X_I2`|48Tk)8SDAK`m`mpkn8PWGf^r-!|7?fj0g1!6T#-=*c zbTfvwY#Gxc)FrvNXnE7$|Mg%E?YdBW^{d5g&n;nql!)z1Ye{PuZlho8U*yaNqvhh- zWD+!tHS0|$^2BHZ{C72MdD!caJ7X|N=Xpk{uq+M1$(wRq+H#_3wucvdrw33Ku4}@n z_y8;-Tz%ApTb2GW8K*lF0d_>OI&xcNY1(Reqw7n2K-owYs-%;8+|}Z*{99bCQRE&{ zDb7`@j?Woj5cbiqfJugBM%`~jL_-r%1UOg*d$$E!de|dI3Ol%*Ot2fyU^60yJc#0H8tr z2hcG51vCQxHK4h1m45;NjqQI0XcPcI1Hd4Fy#)Y1Nn8r?RY~7(+XXFnKmi4{Zr8VNtFLz%4q&i1kDCOQp`B% zlV$yzq)2tN>S`2r+W2!VSG(}!q|sGxJI3A$##N!<42k7g$1SF}Ft6EjU2$CqVU}xT8eOu(hQQ#b z%(s|r^OG>A6(n<3eaQBg%RZX}1baK=ft27S?N61h$6a#A^mI1lWRGUgg6!2mFY+Ty z#!Y`ZkDxgPpU?7>i0RQ2q5?7wAwUyEgwlXDBjU78f}yViGU-XVnTRmvdt`#Ma%A{9 z*0m_4Iczh6_j!xZ-&yy~^j$G0%W4~v0T(3Fa3pq?yxF7z`Xj(dd7TxX*l*a!lX#SwX|F4w+{*MIBBx#$yC;G9`U$Tqeo(Jo=Ma24Cvc~nOgXcQS z-^$Gd)}{4ay#jcWv>Zzgij7rirES5w-$Y^j4V1Lt0ql$>>YlFJ5JpU{=gGv04>`eDBc4kOLB^^%*2%gW9B;=U3 zKDf|abK9+9^UM^Y`+2$7lZ|cvbI)W_#=81pJb!DZ0b%4-58K@i*kB4?5&FRgz{O&# z0SYil685V>x>S><07-1~XW8I3X99miU;sEI`HZG04lz;S)rb^Ty6x3Z*e0+hv4hU> zgT8FFhd{oAJyBq1aTY|zeHH zY~SP`0J55`oBV{yxCKD1`Ob9)5KLt8iTb^ycVBWJw& zWh$9%6Wqb)HI4{4na!>Aiw}=;nbjknYXaQwmO8icpS*$fuu6vh5Hu`*(Cpdi{%iDX zl>@Q;1Td}v=%|O8he)0V7Bt@gl>HV|>hiKOvVRvU*ifJm;pri&&OFUBMj(9Nrsdl= z>_F!%FYXDCF7HUj8=+9`t7aY=FR%h!_f|~(N_0a$|GXp_LPt@=pZdV^pDN9g)-UN2ZBEC&tV?;pnYQA;uPGue z8$_%5@b@3R>bBt=>h|FssqFCv*{K!$PcV_>8}^b<)E8i^9s4dm$&=zfX*r!UI#s%v z!yV*C1;a#v4Y9!VDb3IZ)1msM0_qq7Pn>z5So;{oM~rXW#;hLSMbUp&pP_Dzsrh&h zr=ssFrdR0(gkDq)QlVeF{!BHmEeCF!0P!C+j$ngz_{;1%{3IiasAz9);#h{@KZTTGsZC`+&$)g%37M9a;l`BDkTPD zTu2!&S_NAnM}7i*{3N5e4I^h>z6VRy&}raBl)n$`>UX4xbkxVB@(@1hfhSDm;>UNh zG5M>8vYzE@SN6;LNPU~SwZ)vyjyXQ5QP1K2#Fs{tD)O#ekA>2Hs`G718=L$DR=TP- z6R;1q1#v$=$|vb=iAYbOq>cG<&^?HI^ogHHc;@6qd*}z-3)r)cYc{alch;F#vB#Hi zO3NSLjX&Ju-Y!N@y|oQL+|%AJwC=mA>OTC&-VrEXcn(iLbZS2YQr__R@4ZcGKd|DA zO-EM49Y&7;i?e>i&0VdV&L|@9AiGtcmhfTKL*F^$`*w=3B;Abk_RPnG(~Hq-(hKw4 ztKG`D)$GBk5SQJ(PQd+K;+ur`dFx+IK{vxJJ9P~S6@}|tAszRYEe&7WWLTFAONJmI zelhJ`X)Z(Eas6D1YP~{a3tI15-Ke`}TJW9xhs^Z|kh$P*&$#WyoYD+bZ%6M1{?2c; z2!HcipKyX??c(Yu!E;$wCGV}i`)(S<-#*jt%8%4C=iJ#YFYMv>^``Ka(L@&S`z%2< zAA*qxJ+B2-7TTI37S(W@SIl&NO&euND zSS!#(Y1w7YZ4qtLX(T3B4!WxNaW!y`Gh#P@{EEgtk+cZ6JO zV=D^v1wxES6Wd&}nN3u0DO|y%Kfn$Oj_dZ=Ep&&OUiCUe?#XyjyV<_MmjOPaU>B)B z_PYBAWhh)fOsIXZ0EdqNwp_R;4XHl$Wjw#03^J0?rJv*RBBiRaS4J<#2Lz99=qmQ+ zCo@FobN(OTG)k~FL{yibi+$3-`iuDrgZbbXKr8?5(!R#rCY~n*Z@7Y`zXfMEK`NH= z&(@Bv1i8v57m}1lhoV2~wlBzD1b>&GVKhSgYz)P<=i97{{TcjRU|J2+3^k)5TX}jN zV-w?hIj}P-GRY^&R$j#7f|xF+znle6wof{EoiJ*xJ&#>#WMN#*8w-2grO5p< zHrnzu;s-diU9(b_UdE_8CdUBIZIcT|{eN|6r}Vy1?fo(?x#X58cu;F??!=J(lnz8! zr_=j=NEaY@v69=u?g05utFYd4l(aSaQM_&iXT2-v-2EoHM`Kl={uDlcFjlazGW_Sm zN3q8$58%?iLH#9Aa34L$0vt;bYJe4Ox726`=&Ac`(<&Om#|!`Vau+UO)OHMfi{6#u zxH`1wCE9n!8`=q1E~qa|kr_lXz@vyk z&z$h8px-}f0!i~jz_Y=jpJ;y5uk=mj*OQ8H0Yh$qLVt2iCI-bGl>G_CXy$Eg?!zGj zYYWWL67EB5fR4o!%y#+Zhg=cUPreKW2rfd+{N7ZxV9M^%Xuuf2N&8*0ez3zYn9M}k zYUtrfKM|hc>_WR^e)xHvQu1|aivCQPQnA|@bRPl*mqvSZu^#n)3=Mkm!AQXGiYMw) zl=LbAdn!P_mMGXw8u>|E0J!-DV%sFsU)o>Gq3-5U42z5pi%|a*p&k~&iHmaM<39HH zm?M8GX1tc89Tw>z#A^WDNbnj6@EoPNk9|Gnh@VW@o=iSHnGijh=v@y`S$S>Y1$v{r zZdFHFjpnu9c5F~K*V|}y=bEHARwXe!!&FK6|1q=sgDZlK@$ah} z|7Y3~{%vDV*qJ|@BINKy&|IIFiAKdebjWIiA~MS6ly{dGqknfPMD!cy&8yMt>6+)$ z6K9=N*InIX8A{h{4i8*I*4_ro%i($D;mPcT^U%dF)=mr+gsAOIs zW4&Isrta7CWVH+8fyvrPE}Y4B5XUFw`#n$|R-DFd7QQyhej%q$ZRE~0zSX|q=ZSuO zwPkx=S9n+vDN9KHfDQ}Vx#Yvd%YBNK;ASZ*;l;L}XB}657!Q)i3^d zF*+D{oyDZ3r^R!pWg164DSa8lyH8s|NIM@Fp(J0_Q&Kwl={)XjUb7#6cH&sBxFvJK zqyfPT8Vp5;%nPE^gGBrTA^n9M2vR~Uc_L@-M;~2~fInE!yn-Lqvw2*}Fj}c42lFz2+;}00 z#-*D3fDcYadg>|facs#jJo$vQlvDBAoULIlu1GWMuo!1I_y?- z$`T4gDd=|pu@5PG9URR@lJ`x>yp)0e3gI&SA!n+0Wgn)wR5UYsa$?gM(;na!u(ej^~vR;+*&C9bu7mr zTbQVNu#4zVV2r%<$27b+>?glDscXowa)vRq3sF3A0U|cKZt|G;MbYWhB~rcGpU{GH%zqxp7uN z?6_6rzurd$w6X#T+P7|fvWqaH>{g#fSq(NWVuW~}AH%i3jd>|f^H^d{qx}hj{$#me z_|G#qPe7y4Ul2su9wdI|GsxnfOcB~&6yMjVq)>66>N_iB1_aCwuqkc8iYb?sLQLrc z;&Fd@Y&_~9BWrFw?at4g{QK!rM&QWv@}%d*`r59RBgcx9TYY2WovWh<;-rAe*(s4# zon@zG+R;QzljWUl=~>DhGkL>>lzV{=p6!d7_lp8Ec0zcPm*ee_f;EIUW&&-@&59&m z{3Y-Q%&wO*w2dhUt~xYFWFUOHo}vmHZk%H-*nFWRN3D(&bA{=2N*!r}O}gbY zbg(nkJn$!6_q0!-C*MR%F&+wb(0lZD=y6bd+t}2O-M|$jI`^lw3kGA!bYFM5ZsMKp ztwX$zuiJu1b`b^$TSamzlM$Vn0Z>ccn#AcoAxqkabf@GS^nHskZ|L`mde(889gIhJ zz8KeGo~#~TP!4In^qdT*TD~26+>4(ax1%2dI+O2+4tX-U_{Wp)J{4o!-~hUZj>ti7 zk&F6^zHcu(r>jX~CV~wti@XDte*2tm+v~yc;_wRwb0&Tg^}Vpp>+&Yp&i+zjaL?@B z{E)WCJ0is)1$*8UeN8r}M@Rr6u<=eSiptG|EK`KBGgJJ6R_-?Z@!tms!@p@D zQgpR3#HW?fGgEM|gr=3or>CR)o#fcrJK(d^|8wzvd}gM9=wknI3UsEap@7xW=yjlS zyYq`#sLU}*y1)sQfPkP$4BuZ;-qj`>l$*YBh(*lE>+NYFcEzfRUT0I+X}+Z2tijFH zbiJO&Oy7`S4pn@)cXL~jAY!nBydI(1IUu4&RvC?NMhTT3Wa5}#TmnZylzRFKjZwd$ zfQn;cHkj7H2Ar-^7L|DcwCKqV|LaDcMB6}j;= zF!6}11IQ08vU=sYkV73+U0q9xV^IfKM--0w#KnNdP!3)b97oL@t-VGR)%l4j$V)a7 zSQk!U2+4;}*mRVNhip3VpRjpl{5MfZ4vRxUMl>2F<%g_qbWcsFBVHj#Y=%B z2A2#>K_C}^k-CQW^)oMjk0hS%X}v|XQiD*GpDzX*gS`|ar4A$-I)SY! zx2$-q9%S`bF*}5qa3ldaWd0u_wQ#T_kYB&%OUpv!P+lMcQ#RIu9N^$OlP?&#lRnMsCLFCGM~TGEy7h7@YWLgM&^p^&Vv~$VCzsVljWT;X|!|n z0cpFCm7(oLEhGFzZ>0+z;aGc>^q^z;dVI0xuq=7PO&g+t4JSi#z=a_-LRuKqXQ)q? zE7>Z_Yc3A&?b+DX_SxFTiD%z~s-`NwTpTo`4N<~n;=}Hj4&qcib+;IlwMk7I2xn*? zs+GO{{`+}U(cZ?Mn6+Cce#=q(&aAaFsS+h5u3V3bp->G+YV6nwxRS6V1Xr}`=vDql zp7Zsh3lpZK8u3GpOkzDxL{owAWf)qc3@}Z6^!5I&p*z_!u2(pX%*4s?E$%IbRzhsA zeF5KrM-=9fxamC39xE2yq%e--Lr~rD;ce_e!a-eFPe&8(wy_$41wkD7*H{9DnE7XK zl5x>=aY0Ezb}ixlYvHLCwY3etDI3tZ3mzl}o$T!cmlvV{Tw8YBaJM3b9Et)#!-1`r z48Nq+onOtFYt}`~@(^3`Vf~O>5F^;Si_cxbUV?by!_ljWH1cK#o85i;uydEleQL`W z8ImK@yRe@mND&z>qzGysV&hCPBw7RZy8YICc&}UUVBvE++jWq$&bzqsw=BVUToJOs zNztM=DD)-##Pe$m*?u8640QNvf7Iie(H}*;6wMM)?Y_^1z!3C zQ4^`1w=}O5%(W;2q16_aq82SjF_dkdLP*w5T^l@{I)RYV=I4`=s8|MtbxBh}| zWmL!|#N=5rFDmE@1S9q&lIxR*mFO#(0%mI+0_tjRi!!E9d_H$i(Hibc`bXh zYovE6R;Mr`D9+};1XNSoo7V(-{b-C}B+KQFXnILxzXFU10cLO2#e+h>47c6Y-JJb= z$LDH;JS9@lJggI=TnYT?E~BhUE*AN~`W%&?dlCnP2f%vaP1bX<{!P*cS=yQ~RM zitk{b@oB|wqrb4o$APP#)vkqulP=8^(@~lFk>%wN)kw>_cOxLnHP+_ih4`bJa$Omr z*jiC<&0Qwc@*d0V@>-)0sZX`A^j8?aawETs$_vO#*#RdEPx6~dMnw2j{4|~65DR$a zSdMm#DGw^IxD1|1h4(^d?TIG#5f*R&YYzFewe_sy2c>e16$>K-bcwvQ?Q4uh)IOd3 zgxNw|R_Lb{E1=Zi-tJcDL$5Z>ppSjlX*4{Y&QzL=JMaj%Bk`4+#)u|cp)7?lRz0@U zLV2@u=W6aC@m_l-m4H! zVzxb@$s|M2p&zce#?MyKFinzL9v~&jwz4I(ujPX7_o)3mm#!;vOo_<0<&)pW>8*^9 zN)$N1T+gM0PD#U15lo_hU?xgAA>=rvHlnHRV|lFDzhU>~8aoQ9H#q3w@#Z8E-Ry#K zGD&Nw`AOr6!uu4MGl@-HOT&`H8Z0#u#_~R9hdOM3mI;(rK1ykV9dcg>$q5PmB3X$Khz)Uj(&avh7!_qN`1rni~vZ5y;j&XK11=EpGK{ zRRC$Tde>UsotUi4R{v*W5_EajoIG*NtH!-u>eEut~bNz{GmL5DG_ zfcQ*IKu|A7x#?(ygiG|y^fFxsL+Q^$P^{3XQ@gJ*NK5&?y2fePgotdve){x@lj(^{ z)Cky|b}vg&v+vF~64&n9sH_SLq`~;JP&`c{kKa!@k38xlBy$HLOOLcF%mi;R7=8dtb|%#MulciT5Ks zxw{3B=y9eWYSV5d`FdS7^!UCRQP7w{_vu1kYT?kn@voW} z$MknudC?FE{Yk4pKRov8U7>5siPHVfwpH_i9Rn28&rQ!2>7gRC;hd#tY`#W}brMtf z@)8p)>z=Frq)ma_6oy&xWOu8Pc;$}z$2IW$jk7p3YEZKdQyb9@|R@?=7LQ^ZIwIc}Y5T zFWWLGrd)5At)^?UuOQMGf1zo@Ak4=X#6UYZGObf2hdH1UFEkO!wR&#n?nD1oUm}IR zD$^i^mRhYPa%8y@UN>LZutI0rZ%=vscRYoVF{viqo> zEI+mP(7oPCxM4;{&E?xyiDvn>-KNT7ddLsL z08{;<7gch2j@X1%y9^kJRGiKB#XvLCkH!Su>pC=7XX?Xb;Pzj4KQh{r8Jj- zHoQq)KLk{HtfO@$tD^xdL)G*(h3aDukfcM`J{yoPOvB9Vnxl6RS;F|`g4~^Yxr_^; z5>L^KgoktH0vK_zf;7jb11y0K{XK7G#TpO8bq9(DW-Z;)mY8B|U>IyfblSI$h<2eC z%H??)miamH zz-M1Txlszfb7MA(NT{YpIV<{M6fKcca#Q3ZRf%D_W%9cAad%%G6-zJ27?F*O#5c}x zDqJNN5oH)3forFx*xFA-KsWSA$)>_*Q?ho|cy!k2xU@7ie`ONB5j$uvBFris%esNZ z&9m?5ejpQ`31?SyD`cmf^Dv8l4l@o}o0Flg8alXDEO0H347K9+EIo{0r{rM$$~Uvf z(Gt5e>y{OPh~>}1%u`eoZG7OhsmCinouw=qT@v9DIgeWt*_g{vZZ>)lN%?j!+G|0< zZEbyr+VM@(ci4WG?V_1!o2yGg)P9u>Ls-LWNN3iG9_D8>JgzG4+QGfV2^dXBuA97! z49VM-1(R4+`LA1=OM{P{9=xo~x5k&}D-pG=o@{3pYDr(0G3|)8#>ypSl<@q#FW{Fe zdd6=2+R=heHN;NtI|q&*r`SzBmQd<}nU)f6%{(V?&!pe_>iD#P-nPm+A9*)+*OBpV zhy)=v*)F6GBbK`QBVDMR20mXYo`x8|Mm`0&kbe(Z+>UtEYDb0GmTmtcvhBx)hBrVu z{aJe;gbf2uSfKvP7cN^0GdTpAqZ5IRe%9BEosTGW4=gR|{uH(^2i_5rCpm{H#-;upPz75+~A&}$W?9e9Abm(YTWgXDM^WgAEJZ2Vl0G@$#+5~=Dn69*K2Jyh+Z4J`D3!@6fW z;iEB*Yh^7XtHrZ!y;Y z6Czf!+MKwqYbuES6 zk2L6;b@x^e$2S%w{UN1xgdls?qiywfOxmY>+-|+afm-JblY5Avx?jV4<$>eg8Mo4% zi;TZ+TC$uxS=Tb|PsF&+WDedNJ2wxL{B+(946UOD2!?u3BOn%q1@A}uM#HM^3|4sB z*!g3tl?nGul(W2W=p5|hNG{iVgQQNvyta=Lft6mhWQ0c-YUNN|oNZoD2k^)6|e%rn}RMPxjS^0 zpO|ZBO^by5=GOW(Mkgv`GUhFpl}5LG`)4L~83y`Br3DqST;K8l#!YSy{QEUbndL_$ zj61(%;VE;e9S0`#TawW=i`~bEtTQ|w(jo8aLBJ{;EmauEO-Bcs9J8vfFnJ3TIea%w6RS zb+jOy*k7$)z1GE*9Y$!51(DKn<9ZWCel%BtgaFFR9B9=riMn&n5<`*=3(@X~aCr=s5X)JG9105xLb&dZAPNPBMd)nfPH*BNdndp2ZSC;z5?hi@-!--d>|I=DSv-OhZzhOgp}%ikK`$u)$pHb>eH1)P>K`)XgiOE-ai zn5INUq(7eIb*RXEe|2hh2I3}j>NCbC%S&{Cz)*L{?VOdcnbSi68iGdQW+I4nx0fN!ED_K-^jqE(0Ev~r3K zWDI2}1LNe!*d2~gwOYRIR!F;!VVO!5%Vzd&90$f)ZpyG4Ee#;CWuJz}p^mYg9ih}~ zPWB@9HH<2hkL1#3XW3MGICl)QwIw}z;6XdJu^dn2f#A{z?63fz%fZ( zz$N(3rVb|fCVc(Ijw}DtFyJ8fjt@EJ^nh;)Y?iAFeM(JOpt?tj)8@ag`S;` zoq>*tosy28l#Y%RaGsR4!GCm$qMfddjiCWxU)29t>tEJ}Fk<>CcN_ryV;)fdS5^O6I1W_!=Q$QSp{`k_EWBm2o>{uG8Xq-W6K+_S6! z8dLlNjPH3=IUkp{9t{b=v^585S5(ntt#gBWpcYOh+VrD8$5|7yhb;ejGz@V z&c*SnWo6Oibzh;)>qSHbr>&s6=W<*|tHWD`Ccx>nLp7IjGd7keRDwv1DG5m20oLPT z)T--5n4Z?@vrY$pj2}@ji5PN!biYjqu~=;6SFzY=k&qgFlceG=Wt?=9O78p8$UK1% z(fv_k7QuLHG?M(Uo2|yk;!7h`R4wh8q>zAzC(!Ld`*)z07_b8bCb1DJnozfzRA3{MW&`*T&*2*q ze&Qf$5An{WVCi860v28HIJNKL*U`Z)!NCh{m0k~~DjgnPzrIchYIZo>JzIL3EUtb& z4rE4gC^Nfzy=(8~nQ^HsNmFp-Xn?D*F{M(lT)AoG#v_;ZQoygBvy0`OS{P~o;gzpu zVthatYFwR>PFSG7T69{)$4(c(1m*-xn^$0^>hnEW>8mL~Opon5?)*w>1h7WG#B!d= z@j}M<_DsRkV`YBBqXlzWiK!5sXsu&9@aA3Zc0h$wKZu^ zj_ZlHpNP8V)>$MMW%;neYnJC895DIQmq2k_Z>?&V*rR(KKw6UYU9V$b@>5lbkV``f zwgC?eH94UL$QD~a(&w@yro!f7X((*^+)jxVw={=|6}Qt5kYy6)7@+iIe#`&^Mj`ew zjFu_kqGc4$*w92n4-?=cb#KH3>OuuMhvz)GjEx*pVTUp=?M~~wC%z1xYUQ6QR<&uZ zzFyMQi|Av-($Tr1=I+^`eGmAVHCY$X^SKSGzbrOUf*zPpqVQ6moI1@8#7|kkf-Hq8 z-Su-_&a~c7SF-8gCmTg;{jTkEysyz9w$k`k{K4ipOKhxgWc3Z|bYopXa@OjvKlf)1J?(6yEmM zxXv-3xnK5VS$P73bFno)5Nv(u$NoeE{?%&qH!M??H#CB#6|*uhbivo4!)Kyn)`F&0 zFm*Hh{S=y31z&?6pAnxPuv6aJ+5xchx4TvT_Z3FgfLFhH#qU>w_!^wTfL%h`I0}x`?i_ zM7R8We25*`bGph9829W(kNZu`xY__$2$wvkup7A3m%q<}>4hr9a0S zpizRl8pwJtWz>~m=JY>LM4eLUp@xJC3WX5zBjtz>cz2+jkV#4+0LNQ%kkmVhm zaRk^oGYc6VFD~!Ik%$-zDHg}eO^7(1K*ryX&mo(RX6w@Lhgo;~B6`XX8|p3&O9+nS zcc_L!5Q?Z9N+pO!2qTwkB9P8bh=>$?C?hA9E_Ed=QvSS87#9~Xv^)pozw59`^w(y4 zdAXS}bhbRhW32CY_#>JVzTF8QnDk$^gKm2c8((`po8OzSeJJ2wYa@+E3jWUx^I*!3 z!-nIM&83+Z7eWS`wSV)!Z>!YnCLvV~eHPE^lHbNBsDG6a)X6jr)Q_I3Ye@DbmQ^M@ zY5ZbBoy8qJFxFc!e`+hEk5GMgXDRelHb0Iu?V3Lh(!*?6d3>OcP&;dxP9ChdUznwr zQa!tlNQPHED;mfktvHVCV$G^O?%zr-tZ+MalZ2>nJM%M`Kk-JHa6+5FqilCA%{GSl zGJ*0dleXQx;PSiq*0zc6`#|qY5_D4YeP3kn&>~Ehhti`*&gEWLK$Z4h*N$o3@gl3< zh^!xUVz<1>4`CBuBUSKGU9fsy%5s0DYRm;*ta Date: Fri, 19 Jun 2026 10:08:57 +0100 Subject: [PATCH 02/18] website: update whitepaper draft --- website/src/credits/whitepaper.pdf | Bin 321343 -> 330067 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/src/credits/whitepaper.pdf b/website/src/credits/whitepaper.pdf index faf4a42aab4dac9328cee6a2d8a6f684e178c96d..fb278b4b5c85a8f1a44d4c34fdba8758e48a6d9b 100644 GIT binary patch delta 101511 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

C;Qt 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 {=MR#42A-1fgi|MzoSr{B8CF7<47 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 T7Uu*W3pg_~Hwq;sMNdWwLEINk delta 93130 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 From 547595041eb661b9038245c082f65e5fb19a6f73 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:36:28 +0000 Subject: [PATCH 03/18] ios: open SimpleX links in chat messages via in-app connect flow (#7101) * ios: open SimpleX links in chat messages via in-app connect flow Tapping an inline SimpleX connection link in message text was dispatched through UIApplication.shared.open. iOS drops an open() of a URL owned by the same app while it is in the foreground (the simplex: scheme and the simplex.chat universal links both belong to this app), so the tap was ignored and never reached the connection flow. Web links (Safari) and mailto:/tel: (other apps) were unaffected, which is why only SimpleX links appeared dead. Route SimpleX links to ChatModel.appOpenUrl instead - the same sink onOpenURL feeds, leading to connectViaUrl/planAndConnect. This matches the connection-link card and the multiplatform clients, which connect in-process rather than via an OS round-trip. Also fixes the same problem for the "Send questions and ideas" and "connect to SimpleX Chat developers" buttons, which open simplexTeamURL (a simplex: link) the same broken way. * docs: plan - justify iOS in-app dispatch for SimpleX links in messages Root cause and justification for opening inline SimpleX links via the in-app connect flow instead of UIApplication.shared.open (undefined re-entry of the same foreground app for a self-owned simplex: URL). --- .../Views/Chat/ChatItem/MsgContentView.swift | 16 ++++- apps/ios/Shared/Views/ChatList/ChatHelp.swift | 4 +- .../Views/UserSettings/SettingsView.swift | 4 +- ...6-19-ios-open-simplex-links-in-messages.md | 65 +++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 plans/2026-06-19-ios-open-simplex-links-in-messages.md diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 9aaff57cc5..11c3c4c3f4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -191,9 +191,14 @@ private func handleTextTaps( } } } - if let index, let (uri, browser) = attributedStringLink(s, for: index) { + if let index, let (uri, browser, simplex) = attributedStringLink(s, for: index) { if browser { openBrowserAlert(uri: uri) + } else if simplex, let url = URL(string: uri) { + // SimpleX links target this same app (simplex: scheme / simplex.chat universal link), + // so UIApplication.shared.open is dropped by iOS while the app is in the foreground. + // Route to the in-app connect flow instead (same sink onOpenURL feeds). + ChatModel.shared.appOpenUrl = url } else if let url = URL(string: uri) { UIApplication.shared.open(url) } else { @@ -203,9 +208,10 @@ private func handleTextTaps( }) } - func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool)? { + func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool, Bool)? { var linkURL: String? var browser: Bool = false + var simplex: Bool = false s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in if index >= range.location && index < range.location + range.length { if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo { @@ -213,6 +219,7 @@ private func handleTextTaps( } else if let url = attrs[linkAttrKey] as? String { linkURL = url browser = attrs[webLinkAttrKey] != nil + simplex = attrs[simplexLinkAttrKey] != nil } else if let showSecrets, let i = attrs[secretAttrKey] as? Int { if showSecrets.wrappedValue.contains(i) { showSecrets.wrappedValue.remove(i) @@ -225,7 +232,7 @@ private func handleTextTaps( stop.pointee = true } } - return if let linkURL { (linkURL, browser) } else { nil } + return if let linkURL { (linkURL, browser, simplex) } else { nil } } } @@ -250,6 +257,8 @@ private let linkAttrKey = NSAttributedString.Key("chat.simplex.app.link") private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink") +private let simplexLinkAttrKey = NSAttributedString.Key("chat.simplex.app.simplexLink") + private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret") private let commandAttrKey = NSAttributedString.Key("chat.simplex.app.command") @@ -392,6 +401,7 @@ func messageText( attrs = linkAttrs() if !preview { attrs[linkAttrKey] = simplexUri + attrs[simplexLinkAttrKey] = true handleTaps = true } if let s = text ?? (privacySimplexLinkModeDefault.get() == .description ? linkType.description : nil) { diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift index 3047572236..5844fd3ff9 100644 --- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift +++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift @@ -26,7 +26,9 @@ struct ChatHelp: View { Button("connect to SimpleX Chat developers.") { dismissSettingsSheet() DispatchQueue.main.async { - UIApplication.shared.open(simplexTeamURL) + // simplexTeamURL targets this same app; route to the in-app connect flow + // (UIApplication.shared.open is dropped for self-owned URLs in the foreground) + ChatModel.shared.appOpenUrl = simplexTeamURL } } .padding(.top, 2) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index c1bc699261..483ca6aea8 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -390,7 +390,9 @@ struct SettingsView: View { Button("Send questions and ideas") { dismiss() DispatchQueue.main.async { - UIApplication.shared.open(simplexTeamURL) + // simplexTeamURL targets this same app; route to the in-app connect flow + // (UIApplication.shared.open is dropped for self-owned URLs in the foreground) + ChatModel.shared.appOpenUrl = simplexTeamURL } } } diff --git a/plans/2026-06-19-ios-open-simplex-links-in-messages.md b/plans/2026-06-19-ios-open-simplex-links-in-messages.md new file mode 100644 index 0000000000..f7c89b303a --- /dev/null +++ b/plans/2026-06-19-ios-open-simplex-links-in-messages.md @@ -0,0 +1,65 @@ +# iOS: open SimpleX links in chat messages via in-app connect flow + +## Problem + +On iOS, tapping a **SimpleX connection/invitation link inside message text** does nothing — it never reaches the connection flow. Reproduced on iPhone 17 (v6.5.2 and v6.5.5). On the same screens, tapping a web link (opens browser), a `mailto:`/`tel:` link, and the connection-link **card** all work. Notably it was **device-specific**: dead on an iPhone 17 but working on an iPhone 12 running the **same iOS version**, with only **one** SimpleX app installed. + +## Root cause + +Inline links are dispatched in `MsgContentView.handleTextTaps` (`apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift`): + +- web links (`webLinkAttrKey`) → `openBrowserAlert` → `UIApplication.shared.open` (Safari) +- everything else → `UIApplication.shared.open(url)` + +SimpleX links fell into the second branch. Two facts make this the bug: + +1. **The URI is always the `simplex:` custom scheme.** The core markdown parser normalizes every connection link to the `simplex:` scheme via `simplexConnReqUri` / `simplexShortLink` (`src/Simplex/Chat/Markdown.hs:344,353`), regardless of whether the message contained `https://simplex.chat/…` or `simplex:/…` (see `tests/MarkdownTests.hs`). So the tap always calls `UIApplication.shared.open("simplex:/contact#…")`. + +2. **`simplex:` is registered to this app, and the app is in the foreground.** `UIApplication.shared.open` is an OS app-launch API: it asks iOS (LaunchServices) to resolve the scheme to its registered app and activate it. Here the registered app is SimpleX itself, already foregrounded. **Re-entering the same foreground app through `open()` is not a supported operation** — `open()` exists to hand a URL to a *different* app or the system. When the resolved target is the calling foreground app, the outcome is undefined: on some devices iOS still delivers the URL to `onOpenURL`, on others it is a silent no-op (`open` returns `false`, no error, no UI). + +That undefined outcome is decided by device-local OS state (scheme resolution / launch services), which is why identical code + identical OS + identical single app behaved differently on the iPhone 12 (delivered → connected) and the iPhone 17 (no-op → dead). It is **not** an OS-version rule and **not** a multiple-handler conflict — both were ruled out (same OS; single install). + +This also explains the full symptom matrix — only the path that re-enters the same app via `open()` is affected: + +| Tapped | Dispatch | Target | Result | +|---|---|---|---| +| Web link | `openBrowserAlert` → `open()` | Safari (other app) | works | +| `mailto:` / `tel:` | `open()` | Mail / Phone (other apps) | works | +| Invite card | `planAndConnect` in-process | this app, no `open()` | works | +| Inline SimpleX link | `open("simplex:…")` | this app (self), foreground | undefined → dead | + +The underlying cause is using the **wrong mechanism**: an OS hand-off API to perform an **in-app** action. Every other connect path handles the connection in-process and never leaves the app: + +- the card: `planAndConnect` directly (`FramedItemView.swift`) +- the share extension: `ShareSheet.openExternalLink` sets `ChatModel.appOpenUrl` +- multiplatform: `openVerifiedSimplexUri` → `connectIfOpenedViaUri` → `planAndConnect` + +Inline links were the lone exception delegating to the OS, making them hostage to undefined self-open behavior. + +## Fix + +Restore the three-way dispatch the multiplatform clients use (`WEB_URL` / `OTHER_URL` / `SIMPLEX_URL`): + +- web → `openBrowserAlert` (unchanged) +- `mailto:` / `tel:` → `UIApplication.shared.open` (unchanged — these target other apps) +- **SimpleX → `ChatModel.appOpenUrl`** — the same sink `onOpenURL` feeds, leading to `connectViaUrl` → `planAndConnect`, entirely **in-process** with no OS round-trip + +SimpleX links are identified by a dedicated attribute key (`simplexLinkAttrKey`) set on the `.simplexLink` format, mirroring the multiplatform `SIMPLEX_URL` annotation tag, rather than sniffing the URL string — so all link types (contact, invitation, group, channel, relay) are covered. + +This is correct regardless of the exact device-local trigger, because it removes the dependency on iOS re-delivering a self-owned URL. The invite card already proves the in-process path works on the affected device. + +Also fixes the same issue for the **"Send questions and ideas"** (Settings) and **"connect to SimpleX Chat developers"** (chat help) buttons, which opened `simplexTeamURL` (a `simplex:` link) the same broken way. + +## Scope + +- `apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift` — three-way tap dispatch + `simplexLinkAttrKey` +- `apps/ios/Shared/Views/UserSettings/SettingsView.swift`, `apps/ios/Shared/Views/ChatList/ChatHelp.swift` — route `simplexTeamURL` in-process + +No behavior change for web / `mailto:` / `tel:` links. + +## Verification + +- Tap an inline SimpleX invitation/contact link in a received message → the connection sheet opens (on iPhone 17, where it was previously dead). +- The two developer-contact buttons open the connect flow. +- Web links still open the browser; `mailto:`/`tel:` still open Mail/Phone. +- Optional, to confirm the device-local nature: open a `simplex:/contact#…` link from another app (e.g. Notes) on the affected device — if that is also dead there but works on a second device, it confirms the difference is device-local scheme resolution rather than app code. From 974fbae298dd82f31b6a5e3df6f61ffd3241fd23 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:56:55 +0400 Subject: [PATCH 04/18] ci: clean simplexmq submodule source dir in windows lib build (#7099) --- scripts/desktop/build-lib-windows.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index af408d4054..fdf7154491 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -38,8 +38,9 @@ scripts/desktop/prepare-openssl-windows.sh openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') rm -rf $BUILD_DIR 2>/dev/null || true -# Existence of this directory produces build error: cabal's bug -rm -rf dist-newstyle/src/direct-sq* 2>/dev/null || true +# Existence of these directories produces build error: cabal's bug +# (simplexmq is removed because cabal cannot delete its read-only git submodule pack files - blst, libbbs - on Windows) +rm -rf dist-newstyle/src/direct-sq* dist-newstyle/src/simplexmq* 2>/dev/null || true rm cabal.project.local 2>/dev/null || true echo "ignore-project: False" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local From 2df131bd05281d0ba3b1612141aa9161acec9894 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:13:56 +0400 Subject: [PATCH 05/18] docs: add channel webpage setup guide (#7097) * docs: add channel webpage setup guide Step-by-step guide for channel owners to set up a web preview page: enable the webpage in the app, copy the generated embed code, and host the page. Includes the customizable data-* attributes and troubleshooting. Linked from the user guide contents. * docs: reorder channel webpage steps to recommended flow Set up embedding first to build and test the page from any address, and set the webpage URL last so the link is only shown to subscribers once the page works. Also drop the incorrect 'Save and notify channel subscribers' prompt references (that dialog only appears when leaving with unsaved changes, not on Save). --- docs/guide/README.md | 1 + docs/guide/channel-webpage.md | 127 ++++++++++++++++++++++++++++ website/src/_data/docs_sidebar.json | 1 + 3 files changed, 129 insertions(+) create mode 100644 docs/guide/channel-webpage.md diff --git a/docs/guide/README.md b/docs/guide/README.md index 04e7538968..ad3849d1d3 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -10,6 +10,7 @@ The first messaging platform that has no user identifiers of any kind — 100% p - [Quick start](#quick-start) - scroll down this page - [Sending messages](./send-messages.md) - [Secret groups](./secret-groups.md) +- [Channel webpage](./channel-webpage.md) - [Chat profiles](./chat-profiles.md) - [Managing data](./managing-data.md) - [Audio & video calls](./audio-video-calls.md) diff --git a/docs/guide/channel-webpage.md b/docs/guide/channel-webpage.md new file mode 100644 index 0000000000..8cb0f4fbe5 --- /dev/null +++ b/docs/guide/channel-webpage.md @@ -0,0 +1,127 @@ +--- +title: Channel webpage +--- +# Channel webpage + +A channel webpage shows a preview of your channel on the web: its name, description, recent messages and subscriber count. Visitors can see what the channel is about before they subscribe, and the page gives them a "Join" button along with links to download the app. + +You don't have to build the preview yourself. The chat relays that host your channel publish its content as a small file, and a ready-made script renders it on your page. All it takes is to copy the code the app generates and paste it into a web page you host. + +## What you need + +A channel webpage can be set up by the **owner** of the channel, as long as the channel is hosted on chat relays that support webpages. If they don't, the app shows "Used chat relays do not support webpages." and no code is generated. + +You'll also need somewhere to publish an HTML page. Your own site works, but any static hosting will do. + +## Step 1. Open the channel webpage settings + +Open the channel and tap its name at the top to open the channel information. Scroll down to **Advanced options** and tap **Channel webpage**. This button is only shown to channel owners. + +## Step 2. Allow embedding while you build the page + +Turn on **Allow anyone to embed** and tap **Save**. + +With this on, the relay serves your channel preview to any page, so you can build and test from wherever the page lives without it being tied to one domain yet. Leave the webpage URL empty for now; nothing is shown to your subscribers until you set it. + +## Step 3. Copy the code + +Under **Webpage code** you'll see a snippet like the one below. Tap **Copy code**. + +```html +

+ +``` + +Everything specific to your channel is already in the code: its link, its ID, and the relay domains that serve the preview. There's no need to edit those values. + +## Step 4. Add the code to your page and test it + +Paste the snippet into the page you're going to publish. Here's a complete minimal page: + +```html + + + + + + My Channel + + + +
+ + + +``` + +Publish the page and open it in a browser. The channel preview shows up in place of the `
`. Because embedding is still open, it loads no matter which address you test from, so you can adjust the page until it looks right. + +## Step 5. Set the webpage URL and lock it down + +Once the page works, go back to **Channel webpage** in the app: + +- Under **Enter webpage URL**, type the address where the page is published, for example `https://example.com/my-channel`. +- Turn **Allow anyone to embed** off if you don't want other sites to be able to show your channel preview. Leave it on if you're happy for anyone to embed it. +- Tap **Save**. + +The URL now appears as a link in your channel info that every subscriber can see. If you turned embedding off, the relay also restricts the preview to your own domain. + +## Customizing the preview + +You can add optional `data-*` attributes to the `
` to change how it looks. Only `data-channel-id` and `data-relay-domains` are required, and both are already in the generated code. + +| Attribute | Values | Default | What it does | +| --- | --- | --- | --- | +| `data-channel-id` | channel ID (from the app) | none | Required. Identifies the channel to load. | +| `data-relay-domains` | comma-separated domains | none | Required. Relay domains that serve the preview, tried in order. | +| `data-channel-link` | channel link | none | Enables the "Join" button and QR code. Recommended. | +| `data-app-download-buttons` | `on`, `off` | `on` | Shows or hides the app download buttons. | +| `data-color-scheme` | `light`, `dark`, `site` | `light` | Color theme. `site` follows your page's theme (it uses dark styling when a parent element has the `dark` CSS class). | +| `data-light-background` | CSS color | `#ffffff` | Background color in light mode. | +| `data-dark-background` | CSS color | `#000832` | Background color in dark mode. | +| `data-relay-scheme` | `https`, `http` | `https` | Protocol used to load the preview from the relays. Leave it as `https`. | + +For example, here's a dark theme with a custom background and the download buttons hidden: + +```html +
+ +``` + +## Good to know + +The preview updates on its own. The relays republish channel content periodically, so the page picks up new messages without any change on your side. It's a read-only snapshot, so visitors can't post to it. + +Only what the channel already shows publicly is included: recent messages, member display names and avatars, reactions, and the subscriber count. Deleted and disappearing messages are never published. + +If your channel is served by more than one relay, all of them are listed in `data-relay-domains`. The script tries them in order, so the preview still loads when one relay is unavailable. + +## If something doesn't work + +If the preview area stays empty, check that the page is hosted on the same domain as the URL you set in Step 5, or turn **Allow anyone to embed** back on while you sort it out. The relay only lets that domain load the preview. + +If the app says "Used chat relays do not support webpages.", the relays hosting your channel don't support this feature yet, so no code can be generated. + +If there's no **Channel webpage** button, remember that it only appears for channel owners on channels hosted on relays. diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index f9b4d15b54..e640b7df71 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -6,6 +6,7 @@ "README.md", "send-messages.md", "secret-groups.md", + "channel-webpage.md", "chat-profiles.md", "managing-data.md", "audio-video-calls.md", From 134e48fe7e036be8a8125b3aa8dc342ecd7c681d Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:50:46 +0000 Subject: [PATCH 06/18] android, desktop, ios: remove right gap on received messages in channels (#7106) * android, desktop, ios: remove right gap on received messages in channels In channels received messages now use the full row width instead of the chat-bubble right gap, matching the broadcast/feed style. Gated on ChatInfo.isChannel (useRelays), the always-present channel flag used across the channel UI; sent messages and non-channel groups, business and direct chats are unchanged. * docs: add plan justifying removing right gap on received messages in channels --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Views/Chat/ChatView.swift | 3 +- .../simplex/common/views/chat/ChatView.kt | 8 +- ...06-19-channel-received-remove-right-gap.md | 95 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 plans/2026-06-19-channel-received-remove-right-gap.md diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index efe26fdf89..49643745b0 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -895,8 +895,9 @@ struct ChatView: View { } } else { let voiceNoFrame = voiceWithoutFrame(ci) + let channelReceived = !ci.chatDir.sent && cInfo.isChannel let maxWidth = cInfo.chatType == .group - ? voiceNoFrame + ? voiceNoFrame || channelReceived ? (g.size.width - 28) - 42 : (g.size.width - 28) * 0.84 - 42 : voiceNoFrame diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 990703ac43..a020f8c1fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -2000,7 +2000,7 @@ fun BoxScope.ChatItemsList( Column( Modifier .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = 8.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .fillMaxWidth() .then(swipeableModifier), verticalArrangement = Arrangement.spacedBy(4.dp), @@ -2079,7 +2079,7 @@ fun BoxScope.ChatItemsList( } Row( Modifier - .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) .then(swipeableOrSelectionModifier) ) { @@ -2092,7 +2092,7 @@ fun BoxScope.ChatItemsList( Column( Modifier .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = 8.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .fillMaxWidth() .then(swipeableModifier), verticalArrangement = Arrangement.spacedBy(4.dp), @@ -2162,7 +2162,7 @@ fun BoxScope.ChatItemsList( } Row( Modifier - .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) .then(swipeableOrSelectionModifier) ) { diff --git a/plans/2026-06-19-channel-received-remove-right-gap.md b/plans/2026-06-19-channel-received-remove-right-gap.md new file mode 100644 index 0000000000..5ada84b2b0 --- /dev/null +++ b/plans/2026-06-19-channel-received-remove-right-gap.md @@ -0,0 +1,95 @@ +# Remove the right gap on received messages in channels + +## Problem + +In groups, received messages are laid out as left-aligned chat bubbles whose +maximum width is capped well short of the right edge, leaving a large empty gap +on the right so long content wraps early. In channels this wastes horizontal +space — channel posts are broadcast/feed-style content that reads better using +nearly the full row width. + +## Change + +For channels only, received messages drop the right-side gap so content can use +nearly the full row width (a small edge margin remains). This only changes the +maximum available width: long text uses more of the row, short messages still +size to content, and media stays within its existing cap. Sent messages keep +their existing layout. + +### Android / desktop (`apps/multiplatform`) + +The `end` padding becomes `12.dp` (the same edge margin sent messages use) +instead of `adjustTailPaddingOffset(66.dp, …)`, at the four received-message +layout sites in `ChatItemsList` (`ChatView.kt`): the `GroupRcv` +(member-attributed) and `ChannelRcv` (unattributed) branches, each with and +without an avatar. + +```kotlin +end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp + else adjustTailPaddingOffset(66.dp, start = false) +``` + +### iOS (`apps/ios`) + +iOS computes one per-message `maxWidth` in `ChatView.swift` and applies it to +every bubble; the `* 0.84` factor is the gap. For a received message in a +channel that factor is dropped (full width minus the avatar inset) — the same +geometry the voice-message case already uses: + +```swift +let channelReceived = !ci.chatDir.sent && cInfo.isChannel +let maxWidth = cInfo.chatType == .group +? voiceNoFrame || channelReceived +? (g.size.width - 28) - 42 +: (g.size.width - 28) * 0.84 - 42 +: ... +``` + +The received check (`!ci.chatDir.sent`) is explicit here because, unlike the +Kotlin layout (which has a separate received branch), iOS shares one `maxWidth` +between sent and received. + +## Why gate on `ChatInfo.isChannel` (`useRelays`) + +The change is gated per chat on `ChatInfo.isChannel`, which is +`groupInfo?.useRelays == true` — `chatInfo.isChannel` on both Android/desktop and +iOS (`cInfo.isChannel`). + +This is the robust signal. The whole channel feature on both platforms keys on +`useRelays` (channel preferences, member management, info view, broadcast +compose, etc.); `useRelays` is a non-optional `Bool` that is always present on a +group. + +- **Not on the group-type `isChannel`** (`publicGroup?.groupType == channel`). + This was the first attempt and it left the gap in place on iOS. The likely + mechanism: `publicGroup` is an optional reconstructed from nullable DB columns + (`src/Simplex/Chat/Store/Groups.hs` `toGroupProfile`, plus a creation path that + sets `publicGroup = Nothing`), so when it is not populated for a chat the + optional chain silently evaluates to `false` and the gap is never removed. + `useRelays` cannot fail this way — it is a required `Bool` set at group + creation (`useRelays = not direct`, `Commands.hs:2080`). Independent of the + exact mechanism, `useRelays` is the safer signal. It is also as precise: the + only group type ever constructed is `GTChannel` (`GTGroup` is defined but never + instantiated), and `useRelays == true` is set on exactly that same + public-group/channel path, so `useRelays == true` ⟺ "is a channel" for every + chat today — regular groups, business chats and direct chats all have + `useRelays` false/absent (verified: no non-channel path sets it true). +- **Not on the item direction.** The unattributed `ChannelRcv` direction is + produced for any group message without an attributed member, not only in + channels, and channels also contain member-attributed (`GroupRcv`) posts. + Gating on direction would both over- and under-match, so the gate is the + per-chat `isChannel`. + +## Scope + +Regular groups, business chats, and direct chats are unchanged (`isChannel` is +false for them). Sent messages are untouched. + +## Verification + +- Android/desktop: `:common:compileKotlinDesktop` compiles clean. +- iOS: change is a small, type-safe Swift expression; build/verify on macOS + (Xcode) — not compilable on the Linux build host used here. +- Visual (both platforms): in a channel, long received messages widen toward the + right edge; in a regular group and in direct chats the right gap is unchanged; + sent messages are unchanged everywhere. From 8c4580ee00b1239d6781dccb278534a1284eca47 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 20 Jun 2026 20:54:34 +0100 Subject: [PATCH 07/18] core: block obfuscated simplex links if the group does not allow them (#7107) * core: block obfuscated simplex links if the group does not allow them * remove newlines * remove renames * name * more efficient parser * remove comment --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- src/Simplex/Chat/Library/Commands.hs | 4 ++-- src/Simplex/Chat/Library/Internal.hs | 9 +++++---- src/Simplex/Chat/Library/Subscriber.hs | 6 +++--- src/Simplex/Chat/Markdown.hs | 11 +++++++++++ src/Simplex/Chat/Types.hs | 6 ++++++ tests/ChatTests/Profiles.hs | 6 ++++++ tests/MarkdownTests.hs | 19 +++++++++++++++++++ 7 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 9fe2342dd6..e11a8bd523 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -3675,7 +3675,7 @@ processChatCommand cxt nm = \case profileToSend <- presentUserBadge user incognitoProfile $ case gInfo_ of Just gInfo_' -> - let allowSimplexLinks = maybe True (groupFeatureUserAllowed SGFSimplexLinks) gInfo_' + let allowSimplexLinks = maybe True groupUserAllowSimplexLinks gInfo_' in userProfileInGroup' user allowSimplexLinks incognitoProfile Nothing -> userProfileDirect user incognitoProfile Nothing True chatEvent <- case gInfo_ of @@ -3988,7 +3988,7 @@ processChatCommand cxt nm = \case conn <- createRelayConnection db cxt user (groupMemberId' relayMember) connId ConnPrepared chatV subMode pure (relayMember, conn, groupRelay) let GroupMember {memberRole = userRole, memberId = userMemberId} = membership - allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + allowSimplexLinks = groupUserAllowSimplexLinks gInfo GroupMember {memberId = relayMemberId} = relayMember membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership let relayInv = GroupRelayInvitation { diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 68e870a7c5..79fff87e5b 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -367,7 +367,7 @@ prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Bool prohibitedSimplexLinks gInfo m mc ft = not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) - && (isChatLink mc || maybe False (any ftIsSimplexLink) ft) + && (isChatLink mc || maybe False (any ftIsSimplexLink) ft || hasObfuscatedSimplexLink (msgContentText mc)) where isChatLink = \case MCChat {} -> True @@ -1177,7 +1177,7 @@ introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn sendGroupMemberMessages user gInfo conn introEvts' userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile -userProfileInGroup user = userProfileInGroup' user . groupFeatureUserAllowed SGFSimplexLinks +userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks {-# INLINE userProfileInGroup #-} userProfileInGroup' :: User -> Bool -> Maybe Profile -> Profile @@ -1195,7 +1195,7 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a memberKey = MemberKey <$> memberPubKey } where - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && groupFeatureMemberAllowed SGFDirectMessages m g redactedMemberProfile :: Bool -> Profile -> Profile redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDescr, image, peerType, badge} = @@ -1203,6 +1203,7 @@ redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDes where removeSimplexLink s | allowSimplexLinks = Just s + | hasObfuscatedSimplexLink s = Nothing | otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s sendHistory :: User -> GroupInfo -> GroupMember -> CM () @@ -2129,7 +2130,7 @@ sendGroupMessages user gInfo scope asGroup members events = do _ -> False sendProfileUpdate = do let members' = filter (`supportsVersion` memberProfileUpdateVersion) members - allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + allowSimplexLinks = groupUserAllowSimplexLinks gInfo -- shouldSendProfileUpdate excludes incognito membership, so the badge is presented profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index f8cb2b861c..79ca91918f 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -813,7 +813,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemInfo memId _memProfile | sameMemberId memId m -> do let GroupMember {memberId = membershipMemId} = membership - allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + allowSimplexLinks = groupUserAllowSimplexLinks gInfo membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership -- TODO update member profile -- [async agent commands] no continuation needed, but command should be asynchronous for stability @@ -2701,7 +2701,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure m where contentChanged = not (sameProfileContent (redactedMemberProfile allowSimplexLinks (fromLocalProfile p)) (redactedMemberProfile allowSimplexLinks p')) - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo && groupFeatureMemberAllowed SGFDirectMessages m gInfo updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of Just bc | isMainBusinessMember bc m -> do g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' @@ -3090,7 +3090,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure toMember subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito - let allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + let allowSimplexLinks = groupUserAllowSimplexLinks gInfo membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile -- [async agent commands] no continuation needed, but commands should be asynchronous for stability diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 9507375527..e8cd381941 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -18,6 +18,7 @@ import Control.Monad import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ +import qualified Data.Attoparsec.ByteString.Char8 as AB import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A import Data.ByteString.Char8 (ByteString) @@ -191,6 +192,16 @@ isLink = \case hasLinks :: MarkdownList -> Bool hasLinks = any $ \(FormattedText f _) -> maybe False isLink f +hasObfuscatedSimplexLink :: Text -> Bool +hasObfuscatedSimplexLink t = + fromRight False $ AB.parseOnly findLinkP $ encodeUtf8 $ T.filter (not . isSpace) t + where + findLinkP = do + AB.skipWhile (\c -> c /= 's' && c /= 'h') -- links start only with "simplex:" or "https://" + (True <$ (strP :: AB.Parser AConnectionLink)) + <|> (AB.anyChar *> findLinkP) + <|> pure False + markdownP :: Parser Markdown markdownP = mconcat <$> A.many' fragmentP where diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 7155d407e8..10f492c328 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -638,6 +638,12 @@ groupFeatureUserAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupInfo - groupFeatureUserAllowed feature GroupInfo {membership = GroupMember {memberRole}, fullGroupPreferences} = groupFeatureMemberAllowed' feature memberRole fullGroupPreferences +-- A connection link in a profile description enables a direct connection, so a description +-- keeps its links only when both SimpleX links and direct messages are allowed. +groupUserAllowSimplexLinks :: GroupInfo -> Bool +groupUserAllowSimplexLinks g = + groupFeatureUserAllowed SGFSimplexLinks g && groupFeatureUserAllowed SGFDirectMessages g + mergeUserChatPrefs :: User -> Contact -> FullPreferences mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 0e2052b259..0efdd6baa2 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -2903,6 +2903,12 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil bob <## "bad chat command: feature not allowed SimpleX links" bob ##> ("/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}]") bob <## "bad chat command: feature not allowed SimpleX links" + -- a link split with a space or a newline is still blocked + let (lnk1, lnk2) = splitAt 12 inv + bob ##> ("#team \"" <> lnk1 <> " " <> lnk2 <> "\"") + bob <## "bad chat command: feature not allowed SimpleX links" + bob ##> ("#team \"" <> lnk1 <> "\\n" <> lnk2 <> "\"") + bob <## "bad chat command: feature not allowed SimpleX links" (alice inv <> "\\ntest\"") diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index efa010ceb1..2a5328ff26 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -25,6 +25,7 @@ markdownTests = do textColor textWithUri textWithHyperlink + obfuscatedSimplexLinks textWithEmail textWithPhone textWithMentions @@ -284,6 +285,24 @@ textWithHyperlink = describe "text with HyperLink without link text" do "[click here](example.com)" <==> "[click here](example.com)" "[click here](https://example.com )" <==> "[click here](https://example.com )" +obfuscatedSimplexLinks :: Spec +obfuscatedSimplexLinks = describe "SimpleX links obfuscated with whitespace" do + let addr = "https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw" + inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + let spaced s = T.replace "://" ":// " s -- insert a space right after the scheme + it "detects links split with spaces or newlines" do + hasObfuscatedSimplexLink addr `shouldBe` True + hasObfuscatedSimplexLink (spaced addr) `shouldBe` True + hasObfuscatedSimplexLink (T.intercalate "\n" $ T.chunksOf 8 addr) `shouldBe` True + hasObfuscatedSimplexLink ("connect with me: " <> spaced addr) `shouldBe` True + hasObfuscatedSimplexLink (T.intercalate " " $ T.chunksOf 8 $ "https://simplex.chat" <> inv) `shouldBe` True + it "detects a split link followed by other text" do + hasObfuscatedSimplexLink (spaced addr <> "\nplease connect") `shouldBe` True + it "ignores text without a SimpleX link" do + hasObfuscatedSimplexLink "" `shouldBe` False + hasObfuscatedSimplexLink "hello there, this is a normal message" `shouldBe` False + hasObfuscatedSimplexLink "see https://example.com/page?ref=123 for details" `shouldBe` False + email :: Text -> Markdown email = Markdown $ Just Email From 9b76742c6e1c7f65f55d9b0ce409dcc18e3a2f25 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:49:29 +0000 Subject: [PATCH 08/18] desktop: fix in-app updater deleting the download before "Open file location" (#7104) * desktop: fix updater deleting the download before "Open file location" The in-app updater downloads to a temp UUID file via createTmpFileAndDelete, then relies on `file.renameTo(newFile)` to move the bytes to the asset name so they survive that helper's `finally { tmpFile.delete() }`. The rename's return value was ignored: if it failed, the bytes stayed at the UUID path and the finally block deleted the only copy, so the "Download completed" dialog appeared but "Open file location" opened an empty /tmp/simplex. Use Files.move with REPLACE_EXISTING instead. It performs the same in-place rename when possible (verified: inode preserved, no copy), falls back to copy+delete when an atomic rename isn't possible, and throws on genuine failure - which the existing outer catch handles - instead of silently losing the file. * docs: plan for updater open-file-location fix * docs: plan - note Whonix compatibility (updater previously failed there) --- .../common/views/helpers/AppUpdater.kt | 6 ++- ...26-06-19-fix-updater-open-file-location.md | 48 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 plans/2026-06-19-fix-updater-open-file-location.md diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt index f6a6023d47..c4c34a1db9 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt @@ -321,7 +321,11 @@ private suspend fun downloadAsset(asset: GitHubAsset) { stream.copyTo(output) } val newFile = File(file.parentFile, asset.name) - file.renameTo(newFile) + // Moving instead of renameTo: a bare rename can silently fail (returns false, ignored), + // and the enclosing createTmpFileAndDelete then deletes the only copy in its finally block, + // leaving the user with an empty download dir. Files.move performs the same in-place rename + // when possible, falls back to copy when it can't, and throws (handled below) on real failure. + Files.move(file.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING) AlertManager.shared.showAlertDialogButtonsColumn( generalGetString(MR.strings.app_check_for_updates_download_completed_title), diff --git a/plans/2026-06-19-fix-updater-open-file-location.md b/plans/2026-06-19-fix-updater-open-file-location.md new file mode 100644 index 0000000000..7e4d7e0af1 --- /dev/null +++ b/plans/2026-06-19-fix-updater-open-file-location.md @@ -0,0 +1,48 @@ +# Fix: in-app updater deletes the downloaded file before the user can open/install it + +## Symptom + +Desktop in-app updater (`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt`): after a successful download the "Download completed" dialog appears, but clicking **"Open file location"** opens an **empty** `/tmp/simplex` — the downloaded artifact is gone. Reported against a Linux AppImage build; verified from a terminal that the file was genuinely absent on disk (not a file-manager display glitch). + +## Root cause + +`downloadAsset` writes the download to a temporary UUID-named file created by `createTmpFileAndDelete`, whose contract is to delete that temp file in a `finally` block. To keep the bytes, the code renames the temp file to the asset name so the survivor sits at a *different* path than the one the `finally` deletes: + +```kotlin +createTmpFileAndDelete { file -> // file = /tmp/simplex/ + file.outputStream().use { output -> stream.copyTo(output) } + val newFile = File(file.parentFile, asset.name) + file.renameTo(newFile) // return value IGNORED + ... show "Download completed" dialog ... +} // finally { tmpFile.delete() } +``` + +`File.renameTo` returns a boolean and **its result was ignored**. When the rename succeeds (the common case) the survivor is `newFile` and the `finally` deletes the now-absent UUID path (a no-op) — everything works. But if the rename returns `false`, the bytes stay at the UUID path, `newFile` is never created, and the `finally { tmpFile.delete() }` deletes the only copy. The dialog is still shown (the rename result was never checked), so the user sees "Download completed" over an empty directory. + +The download path is **shared by every platform and asset type** (`.AppImage`, `.deb`, Windows `.msi`, macOS `.dmg`), so this affected all of them — most acutely `.deb`, where the in-app "Install" button is hidden and "Open file location" is the only way forward. + +## Fix + +Replace the unchecked `renameTo` with `Files.move(..., REPLACE_EXISTING)`: + +```kotlin +val newFile = File(file.parentFile, asset.name) +Files.move(file.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING) +``` + +Behaviour: + +- **Same in-place rename in the normal case.** Verified that a same-directory `Files.move` preserves the inode — it executes the same `rename(2)` syscall as `renameTo`, with no copy. No behaviour or performance change on the happy path. +- **Recovers when an in-place rename is not possible** (e.g. cross-filesystem): falls back to copy-then-delete, so `newFile` still ends up present. +- **Surfaces genuine failures** by throwing, which the existing outer `catch (e: Exception)` in `downloadAsset` already handles (logs the error) — instead of silently deleting the download and showing a misleading "Download completed" dialog. + +One line changed (plus a clarifying comment). `Files` / `StandardCopyOption` are already imported. + +## Compatibility impact + +Before this change the updater's download step worked only on *some* Linux systems — it failed on those where `File.renameTo` returns `false`. In particular it did **not** work on **Whonix**, where the "Download completed" dialog appeared over an empty folder and the update could not proceed. `Files.move` succeeds on those systems too (in-place rename when possible, copy+delete otherwise, throwing only on genuine failure), so this fix expands the set of platforms on which the in-app updater works — Whonix included — without changing behaviour where `renameTo` already succeeded. + +## Scope / out of scope + +- This change is limited to the shared download step; it fixes the reported symptom on every OS at once. +- The per-OS *install* paths are untouched. A separate, related fragility remains in the macOS install branch (`File("/Applications/SimpleX.app").renameTo(...)` return value ignored at the app-replace/restore steps); it is a different symptom (botched/missing install, not a lost download) and is left for a follow-up, consistent with the out-of-scope list in `plans/2026-05-16-desktop-updater-fixes.md`. From c42c121a3652d0aac083875311d179f225fb5405 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 21 Jun 2026 13:03:57 +0100 Subject: [PATCH 09/18] core: improve short link decompression (#7110) * fix: bound short-link decompression * core: improve short link decompression --------- Co-authored-by: Paul Bottinelli --- cabal.project | 2 +- src/Simplex/Chat/Library/Internal.hs | 9 ++++----- tests/ProtocolTests.hs | 24 +++++++++++++++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cabal.project b/cabal.project index 66356aa56e..0302d6f7d7 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: df6c53f830853c4cec4c530fe4366f6e7c02e527 + tag: 958de3bfcac2a676f4c2bd38d16d899b174fa8f1 source-repository-package type: git diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 45688ae6e9..c67ad0d514 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -90,7 +90,7 @@ import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..)) -import Simplex.Messaging.Compression (compressionLevel) +import Simplex.Messaging.Compression (compressionLevel, limitDecompress') import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -1450,10 +1450,9 @@ encodeShortLinkData d = decodeLinkUserData :: J.FromJSON a => ConnLinkData c -> IO (Maybe a) decodeLinkUserData cData | B.null s = pure Nothing - | B.head s == 'X' = case Z1.decompress $ B.drop 1 s of - Z1.Error e -> Nothing <$ logError ("Error decompressing link data: " <> tshow e) - Z1.Skip -> pure Nothing - Z1.Decompress s' -> decode s' + | B.head s == 'X' = case limitDecompress' maxDecompressedMsgLength $ B.drop 1 s of + Left e -> Nothing <$ logError ("Error decompressing link data: " <> tshow e) + Right s' -> decode s' | otherwise = decode s where decode s' = case J.eitherDecodeStrict s' of diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 1482a9de10..d76c86495f 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -9,6 +9,7 @@ module ProtocolTests where import qualified Data.Aeson as J import Data.ByteString.Char8 (ByteString) import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) +import Simplex.Chat.Library.Internal (decodeLinkUserData, encodeShortLinkData) import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences @@ -22,7 +23,9 @@ import Simplex.Messaging.Version import Test.Hspec protocolTests :: Spec -protocolTests = decodeChatMessageTest +protocolTests = do + decodeChatMessageTest + shortLinkDataTests srv :: SMPServer srv = SMPServer "smp.simplex.im" "5223" (C.KeyHash "\215m\248\251") @@ -109,6 +112,25 @@ testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = N testGroupProfile :: GroupProfile testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} +shortLinkDataTests :: Spec +shortLinkDataTests = describe "Short link data encoding/decoding" $ do + it "decodes compressed short-link user data below the decompressed size limit" $ do + let value = replicate 11000 'a' + decodeLinkUserData (linkData value) `shouldReturn` Just value + it "rejects compressed short-link user data above the decompressed size limit" $ do + let value = replicate (maxDecompressedMsgLength + 1) 'a' + decodeLinkUserData (linkData value) `shouldReturn` (Nothing :: Maybe String) + where + linkData value = + ContactLinkData + supportedSMPAgentVRange + UserContactData + { direct = True, + owners = [], + relays = [], + userData = encodeShortLinkData (value :: String) + } + decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do it "x.msg.new simple text" $ From 687661313fd4b87e6c60f7f81f9ac3d8e51b231f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 21 Jun 2026 13:31:55 +0100 Subject: [PATCH 10/18] 6.5.6.0 --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 1afaa31e25..06622a65a2 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 44898bf7f6079dbd7f501fd62a31d8cf54792a27 + tag: 92598c2ddb06cfc2c19797a2c900cffcb8af4d5c source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index ebd9d67cc0..8519d08c3c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."44898bf7f6079dbd7f501fd62a31d8cf54792a27" = "1kwwxq2k819xrahjmjzpgv6pff825qpds4r42qjajj5blhv9h6pc"; + "https://github.com/simplex-chat/simplexmq.git"."92598c2ddb06cfc2c19797a2c900cffcb8af4d5c" = "0x98w50vcb2qwdxgkgfvygn1vzclb12zg1k6kcs9rrw54rhmvfmp"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 20b71a052d..6122226bf8 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.5.0 +version: 6.5.6.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 674e7e19c041fac3fb4df273d23cadd67e20b47a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 21 Jun 2026 14:03:08 +0100 Subject: [PATCH 11/18] core: 7.0.0.2 (simplexmq 7.0.0.2) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 0302d6f7d7..516d029b11 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 958de3bfcac2a676f4c2bd38d16d899b174fa8f1 + tag: 98391fd677f547f610a17c48cc96d8c4b2d5e96e source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 08d0e8abf6..8dca57a258 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."df6c53f830853c4cec4c530fe4366f6e7c02e527" = "0xxbs8n0976c75p72pgkixa97isapb9r02xyflmvj8gnmxw4ilay"; + "https://github.com/simplex-chat/simplexmq.git"."98391fd677f547f610a17c48cc96d8c4b2d5e96e" = "1ajgidba08dq8yj7xyr9i47rfrlkzbr75j5nyyjcv0zf778sgv0m"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index db8a65ef5a..0250f3d2dd 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 7.0.0.1 +version: 7.0.0.2 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 6cde614e51e92fde0ea6068a7242fbfa897ec8dd Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 21 Jun 2026 23:36:15 +0100 Subject: [PATCH 12/18] core: fix group link use after admin demotion (#7111) * Fix group link use after admin demotion * fix: group role change * size limit * fix * allow delete * do not remove link * query plan * relay test * refactor --------- Co-authored-by: Paul Bottinelli Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- src/Simplex/Chat/Library/Subscriber.hs | 49 ++++++++++------- src/Simplex/Chat/Protocol.hs | 2 +- .../SQLite/Migrations/chat_query_plans.txt | 13 +++++ tests/ChatTests/Groups.hs | 52 +++++++++++++++++++ 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 79ca91918f..a058857076 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -731,8 +731,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = ct <- getContactViaMember db cxt user m liftIO $ setNewContactMemberConnRequest db user m cReq liftIO $ (ct,) <$> getGroupLinkId db user gInfo - sendGrpInvitation ct m groupLinkId - toView $ CEvtSentGroupInvitation user gInfo ct m + if memberRole' membership >= GRAdmin + then do + sendGrpInvitation ct m groupLinkId + toView $ CEvtSentGroupInvitation user gInfo ct m + else messageError "processGroupMessage: group link host no longer has admin role" where sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> CM () sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do @@ -1535,9 +1538,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do -- TODO [short links] deduplicate request by xContactId? gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId - if useRelays' gInfo - then messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored direct join request from " <> displayName <> " (group uses relays)" - else do + if + | useRelays' gInfo -> + messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored direct join request from " <> displayName <> " (group uses relays)" + | memberRole' (membership gInfo) < GRAdmin -> + messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored join request because host is no longer admin" + | otherwise -> do acceptMember_ <- asks $ acceptMember . chatHooks . config maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case Right (acceptance, useRole) @@ -1566,20 +1572,23 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = createRelayRequestGroup db cxt user groupRelayInv invId chatVRange initialDelay GSMemAccepted RSInvited lift $ void $ getRelayRequestWorker True xGrpRelayTest :: InvitationId -> VersionRangeChat -> ByteString -> CM () - xGrpRelayTest invId chatVRange challenge = do - privKey_ <- withAgent $ \a -> getConnLinkPrivKey a (aConnId conn) - case privKey_ of - Nothing -> eToView $ ChatError (CEInternalError "no short link key for relay address") - Just privKey -> do - let sig = C.signatureBytes $ C.sign' privKey challenge - msg = XGrpRelayTest challenge (Just sig) - subMode <- chatReadVar subscriptionMode - chatVR <- chatVersionRange - let chatV = chatVR `peerConnChatVersion` chatVRange - (cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV - withStore $ \db -> do - Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode - liftIO $ setCommandConnId db user cmdId testCId + xGrpRelayTest invId chatVRange challenge + | isTrue userChatRelay && isNothing ucGroupId_ = + withAgent (`getConnLinkPrivKey` aConnId conn) >>= \case + Nothing -> eToView $ ChatError (CEInternalError "no short link key for relay address") + Just privKey -> do + let sig = C.signatureBytes $ C.sign' privKey challenge + msg = XGrpRelayTest challenge (Just sig) + subMode <- chatReadVar subscriptionMode + chatVR <- chatVersionRange + let chatV = chatVR `peerConnChatVersion` chatVRange + (cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV + withStore $ \db -> do + Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode + liftIO $ setCommandConnId db user cmdId testCId + | otherwise = messageError "relay test sent to non-relay link" + where + User {userChatRelay} = user -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays -- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember) memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Profile -> MemberId -> MemberKey -> CM () @@ -3113,7 +3122,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = where GroupMember {memberId = membershipMemId} = membership changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} gEvent - | senderRole < GRAdmin || senderRole < fromRole = + | senderRole < maximum ([GRAdmin, fromRole, memRole] :: [GroupMemberRole]) = messageError "x.grp.mem.role with insufficient member permissions" $> Nothing | otherwise = do withStore' $ \db -> updateGroupMemberRole db user member memRole diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index f8ccaa74e7..86202fe598 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -932,7 +932,7 @@ parseChatMessages msg = case B.head msg of Right (compressed :: L.NonEmpty Compressed) -> case traverse decompressedSize compressed of Nothing -> [Left "compressed size not specified"] Just sizes - | sum sizes > maxDecompressedMsgLength -> [Left "decompressed size exceeds limit"] + | any (maxDecompressedMsgLength <) sizes || maxDecompressedMsgLength < sum sizes -> [Left "decompressed size exceeds limit"] | otherwise -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1) compressed parseUncompressed' "" = [Left "empty string"] parseUncompressed' s = parseUncompressed (B.head s) s diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 803e012773..e7188f3cc2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -1134,6 +1134,19 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET member_role = 'owner' + WHERE member_category = 'user' + AND group_id IN ( + SELECT group_id FROM groups WHERE local_display_name = 'team' + ) + +Plan: +SEARCH group_members USING INDEX idx_group_members_group_id_index_in_group (group_id=?) +LIST SUBQUERY 1 +SCAN groups USING COVERING INDEX sqlite_autoindex_groups_1 + Query: DELETE FROM chat_item_reactions WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ? diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 82906110c6..c8cd1c5f30 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -81,6 +81,7 @@ chatGroupTests = do it "group live message" testGroupLiveMessage it "update group profile" testUpdateGroupProfile it "update member role" testUpdateMemberRole + it "check owner role change" testOwnerRoleChange it "group description is shown as the first message to new members" testGroupDescription it "moderate message of another group member" testGroupModerate it "moderate own message (should process as deletion)" testGroupModerateOwn @@ -108,6 +109,7 @@ chatGroupTests = do it "invitee incognito" testGroupLinkInviteeIncognito it "incognito - join/invite" testGroupLinkIncognitoJoinInvite it "group link member role" testGroupLinkMemberRole + it "demotion does not remove group link" testGroupLinkDemotedAdmin it "host profile received" testGroupLinkHostProfileReceived it "existing contact merged" testGroupLinkExistingContactMerged describe "group links - member screening" $ do @@ -1608,6 +1610,37 @@ testUpdateMemberRole = alice ##> "/mr team alice admin" alice <## "bad chat command: can't change role for self" +testOwnerRoleChange :: HasCallStack => TestParams -> IO () +testOwnerRoleChange = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + void $ withCCTransaction cath $ \db -> + DB.execute_ + db + [sql| + UPDATE group_members + SET member_role = 'owner' + WHERE member_category = 'user' + AND group_id IN ( + SELECT group_id FROM groups WHERE local_display_name = 'team' + ) + |] + + cath ##> "/mr #team bob owner" + cath <## "#team: you changed the role of bob to owner" + concurrentlyN_ + [ alice <## "error: x.grp.mem.role with insufficient member permissions", + bob <## "error: x.grp.mem.role with insufficient member permissions" + ] + + bob ##> "/ms team" + bob + <### [ "alice (Alice): owner, host, connected", + "bob (Bob): admin, you, connected", + "cath (Catherine): admin, connected" + ] + testGroupDescription :: HasCallStack => TestParams -> IO () testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do connectUsers alice bob @@ -2929,6 +2962,25 @@ testGroupLinkMemberRole = bob <## "#team: cath changed your role from member to admin" alice <## "#team: cath changed the role of bob from member to admin" +testGroupLinkDemotedAdmin :: HasCallStack => TestParams -> IO () +testGroupLinkDemotedAdmin = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob _cath -> do + createGroup2' "team" alice (bob, GRAdmin) True + + bob ##> "/create link #team member" + _gLink <- getGroupLink bob "team" GRMember True + + alice ##> "/mr #team bob member" + concurrentlyN_ + [ alice <## "#team: you changed the role of bob to member", + bob <## "#team: alice changed your role from admin to member" + ] + + -- demotion does not remove bob's group link (it is preserved, usable again on re-promotion) + bob ##> "/show link #team" + void $ getGroupLink bob "team" GRMember False + testGroupLinkHostIncognito :: HasCallStack => TestParams -> IO () testGroupLinkHostIncognito = testChat2 aliceProfile bobProfile $ From f0b9006c54f1e41bed10146d530d4c8bcd1b7f94 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 21 Jun 2026 23:37:11 +0100 Subject: [PATCH 13/18] core: 6.5.6.1 --- simplex-chat.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 6122226bf8..74a96c1371 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.6.0 +version: 6.5.6.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 5d3f0166270c9be845dfacee47fa97dd3abf4536 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 21 Jun 2026 23:38:14 +0100 Subject: [PATCH 14/18] core: 7.0.0.3 --- simplex-chat.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 0250f3d2dd..22b97c83ee 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 7.0.0.2 +version: 7.0.0.3 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 0e09b38ea6834ae31a0e6af5700a9c869c5bb5d8 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:15:41 +0000 Subject: [PATCH 15/18] core: public groups - roster of privileged members (#7017) --- apps/ios/SimpleXChat/ChatTypes.swift | 2 + .../chat/simplex/common/model/ChatModel.kt | 2 + .../commonMain/resources/MR/base/strings.xml | 1 + bots/api/TYPES.md | 13 + bots/src/API/Docs/Types.hs | 2 + bots/src/API/TypeInfo.hs | 1 + docs/protocol/channels-overview.md | 3 +- .../types/typescript/src/types.ts | 8 + .../src/simplex_chat/types/_types.py | 6 +- ...-05-26-public-groups-via-relays-unified.md | 227 +++++ plans/2026-06-01-roster-members-multipart.md | 220 +++++ simplex-chat.cabal | 2 + src/Simplex/Chat/Library/Commands.hs | 134 +-- src/Simplex/Chat/Library/Internal.hs | 254 +++++- src/Simplex/Chat/Library/Subscriber.hs | 583 ++++++++++--- src/Simplex/Chat/Protocol.hs | 106 ++- src/Simplex/Chat/Store/Connections.hs | 2 +- src/Simplex/Chat/Store/Files.hs | 114 ++- src/Simplex/Chat/Store/Groups.hs | 288 ++++++- src/Simplex/Chat/Store/Messages.hs | 11 +- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20260602_group_roster.hs | 64 ++ .../Store/Postgres/Migrations/chat_schema.sql | 75 +- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20260602_group_roster.hs | 63 ++ .../SQLite/Migrations/chat_query_plans.txt | 238 +++++- .../Store/SQLite/Migrations/chat_schema.sql | 40 +- src/Simplex/Chat/Store/Shared.hs | 8 +- src/Simplex/Chat/Types.hs | 39 + src/Simplex/Chat/Types/Shared.hs | 11 + tests/ChatClient.hs | 2 +- tests/ChatTests/Groups.hs | 776 ++++++++++++++++-- tests/ProtocolTests.hs | 10 +- 33 files changed, 2902 insertions(+), 411 deletions(-) create mode 100644 plans/2026-05-26-public-groups-via-relays-unified.md create mode 100644 plans/2026-06-01-roster-members-multipart.md create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20260602_group_roster.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20260602_group_roster.hs diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 56f92cfc0b..d7478e5c3b 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2741,6 +2741,7 @@ public enum RelayStatus: String, Decodable, Equatable, Hashable { case new case invited case accepted + case acknowledgedRoster case active case inactive case rejected @@ -2816,6 +2817,7 @@ extension RelayStatus { case .new: "new" case .invited: "invited" case .accepted: "accepted" + case .acknowledgedRoster: "acknowledged roster" case .active: "active" case .inactive: "inactive" case .rejected: "rejected" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index fa2051a761..ecfb58fdb0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2415,6 +2415,7 @@ enum class RelayStatus { @SerialName("new") New, @SerialName("invited") Invited, @SerialName("accepted") Accepted, + @SerialName("acknowledgedRoster") AcknowledgedRoster, @SerialName("active") Active, @SerialName("inactive") Inactive, @SerialName("rejected") Rejected; @@ -2423,6 +2424,7 @@ enum class RelayStatus { New -> generalGetString(MR.strings.relay_status_new) Invited -> generalGetString(MR.strings.relay_status_invited) Accepted -> generalGetString(MR.strings.relay_status_accepted) + AcknowledgedRoster -> generalGetString(MR.strings.relay_status_acknowledged_roster) Active -> generalGetString(MR.strings.relay_status_active) Inactive -> generalGetString(MR.strings.relay_status_inactive) Rejected -> generalGetString(MR.strings.relay_status_rejected) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 9630c61004..4ea07d4608 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -3022,6 +3022,7 @@ new invited accepted + acknowledged roster active inactive rejected diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 4f3df9d365..b38e3f6c6e 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -87,6 +87,7 @@ This file is generated automatically. - [FileProtocol](#fileprotocol) - [FileStatus](#filestatus) - [FileTransferMeta](#filetransfermeta) +- [FileType](#filetype) - [Format](#format) - [FormattedText](#formattedtext) - [FullGroupPreferences](#fullgrouppreferences) @@ -2100,6 +2101,15 @@ NO_FILE: - cancelled: bool +--- + +## FileType + +**Enum type**: +- "normal" +- "roster" + + --- ## Format @@ -2316,6 +2326,7 @@ MemberSupport: - uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? - customData: JSONObject? - groupSummary: [GroupSummary](#groupsummary) +- rosterVersion: int64? - membersRequireAttention: int - viaGroupLinkUri: string? - groupKeys: [GroupKeys](#groupkeys)? @@ -3317,6 +3328,7 @@ Cancelled: - xftpRcvFile: [XFTPRcvFile](#xftprcvfile)? - fileInvitation: [FileInvitation](#fileinvitation) - fileStatus: [RcvFileStatus](#rcvfilestatus) +- fileType: [FileType](#filetype) - rcvFileInline: [InlineFileMode](#inlinefilemode)? - senderDisplayName: string - chunkSize: int64 @@ -3441,6 +3453,7 @@ ParseError: - "new" - "invited" - "accepted" +- "acknowledgedRoster" - "active" - "inactive" - "rejected" diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 7b268f4ec5..6313c68838 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -270,6 +270,7 @@ chatTypesDocsData = (sti @FileProtocol, STEnum' (consLower "FP"), "", [], "", ""), (sti @FileStatus, STEnum, "FS", [], "", ""), (sti @FileTransferMeta, STRecord, "", [], "", ""), + (sti @FileType, STEnum' (consLower "FT"), "", [], "", ""), (sti @Format, STUnion, "", ["Unknown"], "", ""), (sti @FormattedText, STRecord, "", [], "", ""), (sti @FullGroupPreferences, STRecord, "", [], "", ""), @@ -489,6 +490,7 @@ deriving instance Generic FileInvitation deriving instance Generic FileProtocol deriving instance Generic FileStatus deriving instance Generic FileTransferMeta +deriving instance Generic FileType deriving instance Generic Format deriving instance Generic FormattedText deriving instance Generic FullGroupPreferences diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index 8dfba2bbb0..c5a3b11953 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -170,6 +170,7 @@ toTypeInfo tr = "DBEntityId'" -> ST TInt64 [] "Integer" -> ST TInt64 [] "Version" -> ST TInt [] + "VersionRoster" -> ST TInt64 [] "BoolDef" -> ST TBool [] "PQEncryption" -> ST TBool [] "PQSupport" -> ST TBool [] diff --git a/docs/protocol/channels-overview.md b/docs/protocol/channels-overview.md index d4cd2d2965..7a55f8b6e5 100644 --- a/docs/protocol/channels-overview.md +++ b/docs/protocol/channels-overview.md @@ -182,7 +182,7 @@ The low-level protocol supports multiple owners from the initial release. The ap - **Subscribers** connect to relays and receive content. They cannot send messages by default, but can be given posting rights. -Additional roles (moderator, admin, member, author) exist in the hierarchy and are inherited from the group protocol. +Additional roles (moderator, admin, member, author) exist in the hierarchy and are inherited from the group protocol. The owner-signed roster tracks the promoted set - members, moderators, and admins; subscribers are observers until an owner promotes them. For protocol-level detail - wire formats, message types, signing and verification mechanics, delivery pipeline - see [SimpleX Channels Protocol](./channels-protocol.md). @@ -242,6 +242,7 @@ This threat model assumes the [SimpleX network threat model](https://github.com/ - Undetectably substitute content - subscribers on honest relays receive the original. - Alter the channel's authoritative state on the owner's device. - Substitute the channel profile or impersonate an owner - these require valid signatures. +- Replay an old roster or role change to re-elevate a removed or demoted member for existing subscribers - they reject anything older than the roster version they applied (a new joiner with no prior roster can still be served an old one, until it syncs from another relay). - Redirect subscribers to a different channel - the entity ID is validated across link and profile. - Determine subscriber identity or network address - inherited from SMP transport. - Correlate subscriber participation across channels - each connection uses independent SMP queues. The subscriber chooses their SMP router independently, so collusion between a relay and the relay's SMP router does not compromise connections through a different router. diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 597f2f5503..323f174f91 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2369,6 +2369,11 @@ export interface FileTransferMeta { cancelled: boolean } +export enum FileType { + Normal = "normal", + Roster = "roster", +} + export type Format = | Format.Bold | Format.Italic @@ -2599,6 +2604,7 @@ export interface GroupInfo { uiThemes?: UIThemeEntityOverrides customData?: object groupSummary: GroupSummary + rosterVersion?: number // int64 membersRequireAttention: number // int viaGroupLinkUri?: string groupKeys?: GroupKeys @@ -3635,6 +3641,7 @@ export interface RcvFileTransfer { xftpRcvFile?: XFTPRcvFile fileInvitation: FileInvitation fileStatus: RcvFileStatus + fileType: FileType rcvFileInline?: InlineFileMode senderDisplayName: string chunkSize: number // int64 @@ -3806,6 +3813,7 @@ export enum RelayStatus { New = "new", Invited = "invited", Accepted = "accepted", + AcknowledgedRoster = "acknowledgedRoster", Active = "active", Inactive = "inactive", Rejected = "rejected", diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 9546bfe5a8..25825d8f98 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -1662,6 +1662,8 @@ class FileTransferMeta(TypedDict): chunkSize: int # int64 cancelled: bool +FileType = Literal["normal", "roster"] + class Format_bold(TypedDict): type: Literal["bold"] @@ -1822,6 +1824,7 @@ class GroupInfo(TypedDict): uiThemes: NotRequired["UIThemeEntityOverrides"] customData: NotRequired[dict[str, object]] groupSummary: "GroupSummary" + rosterVersion: NotRequired[int] # int64 membersRequireAttention: int # int viaGroupLinkUri: NotRequired[str] groupKeys: NotRequired["GroupKeys"] @@ -2550,6 +2553,7 @@ class RcvFileTransfer(TypedDict): xftpRcvFile: NotRequired["XFTPRcvFile"] fileInvitation: "FileInvitation" fileStatus: "RcvFileStatus" + fileType: "FileType" rcvFileInline: NotRequired["InlineFileMode"] senderDisplayName: str chunkSize: int # int64 @@ -2667,7 +2671,7 @@ class RelayProfile(TypedDict): shortDescr: NotRequired[str] image: NotRequired[str] -RelayStatus = Literal["new", "invited", "accepted", "active", "inactive", "rejected"] +RelayStatus = Literal["new", "invited", "accepted", "acknowledgedRoster", "active", "inactive", "rejected"] ReportReason = Literal["spam", "content", "community", "profile", "other"] diff --git a/plans/2026-05-26-public-groups-via-relays-unified.md b/plans/2026-05-26-public-groups-via-relays-unified.md new file mode 100644 index 0000000000..91f7c3a6ce --- /dev/null +++ b/plans/2026-05-26-public-groups-via-relays-unified.md @@ -0,0 +1,227 @@ +# Plan: Public groups via relays (unified) + +Date: 2026-05-26 + +This plan is self-contained. It supersedes `2026-05-08-public-groups-via-relays.md` and folds in the privileged-roster mechanism understood since. Implementers should work from this document alone. File:line anchors are current as of this date — confirm before editing. + +## Overview + +Channels (shipped) are relay-mediated groups where the relay forwards content from owners only; subscribers are pinned to `GRObserver` and cannot post. Public groups are the second value of the same two-axis design: same wire, same transport, but every member can post, and there are moderators/admins who can act. + +| `useRelays` | `groupType` | Name | Posting | Notes | +|---|---|---|---|---| +| `false` | (none) | Secret group | all members | today's P2P full-mesh group | +| `true` | `GTChannel` | Channel | owners only | shipped; subscribers anonymous to each other | +| `true` | `GTGroup` | **Public group** | every member | **new**; member-to-member DMs deferred | +| `true` | `GTUnknown _` | (refuse) | — | newer-client link seen by older client → refuse to join | + +Three concepts, kept distinct: **transport** = `useRelays` (topology, batch, signatures, delivery); **governance** = `groupType` (who may post, member affordances); **joiner role** = the default role new joiners get, set by the owner on the signed profile. + +Two things make public groups work and neither exists today: + +1. **The joiner role must come from the owner-signed profile**, not a relay-side global config — otherwise relays disagree on the default role. (Section 2.) +2. **Members must learn who the moderators/admins are — their identity, signing key, and role — in a way a relay cannot forge.** Today a relay can fabricate a moderator (Section 1, Problem). This is the load-bearing piece and ships first. + +`GTGroup`, `PublicGroupProfile`, `useRelays'`, and the relay/signing/forwarding machinery already exist (anchors below). The work is additive. + +--- + +# Section 1 — Privileged roster (`XGrpRoster`) + +This is a **general relay-group mechanism**: public groups use it now; channels inherit it for their multi-owner/moderator future. It is the first, self-contained task. + +## 1.1 Problem and trust model + +Owners are trusted because their keys come from the **link**, never the relay: on join, `createLinkOwnerMember` (`Store/Groups.hs:3072`) writes each owner's `member_pub_key` from the link's `OwnerAuth` chain, validated against `publicGroupId == sha256(rootKey)`. `xGrpMemIntro` even nulls the key when `mRole == GROwner` (`Subscriber.hs:3029`): *"owner key must only come from link data, not from relay intro."* + +Non-owner privileged members have no such anchor. Today `xGrpMemIntro` **keeps** the relay-asserted key for `GRModerator`/`GRAdmin`, and `introduceInChannel` (`Internal.hs:1165`) introduces all of `getGroupModerators` (which returns mod+admin+owner, `Store/Groups.hs:1190`). So a malicious relay can assert "X is a moderator, here is X's key," and the subscriber will then trust the relay-chosen key to verify X's signed administrative actions (`XGrpMemDel`, `XGrpMemRestrict`, `XGrpMemRole`). The `when (memRole > GRMember)` gate in `xGrpMemNew` (`Subscriber.hs:2957`) blocks the *dissemination* path but not the *join-time intro* path — the protection is half-applied. Dormant for channels (single-owner broadcast), activated by public groups. + +**Conclusion:** a non-owner privileged member's `(memberId, name, key, role)` must be **owner-signed**, exactly like owners are link-signed. That is the roster. + +## 1.2 Wire event and signing + +New event `XGrpRoster` (add to `ChatMsgEvent`, `Protocol.hs:422`), JSON-encoded, carrying: + +- `version :: Word32` — monotonic, from 0. +- `roster :: [{ memberId, name, key, role }]` — the complete current privileged set, `role ∈ {GRModerator, GRAdmin}`. `name` is a display name only (to avoid ugly "unknown member" records, as `XGrpMsgForward` already carries one). Owners are **not** in the roster. + +Add `XGrpRoster_` to `requiresSignature` (`Protocol.hs:1231`) ⇒ `True`. This makes the owner sign it via the existing `groupMsgSigning` (`Internal.hs:1962`, binding `CBGroup <> (publicGroupId, ownerMemberId)`, key `groups.member_priv_key`) and makes recipients require a valid owner signature via `withVerifiedMsg` (`Subscriber.hs:3461`). No new crypto. + +**The handler MUST assert the resolved author is an owner** (`memberRole' author == GROwner`). `withVerifiedMsg` verifies the signature against the *author's* key, and the relay chooses `fwdSender` — so without this assertion a relay could route a roster as a member whose key it controls and the signature would verify. Owners exist on recipients only via the link `OwnerAuth` chain, so a relay can neither fabricate an owner nor sign as one. This assertion is the crux of the roster's integrity. + +## 1.3 Authoritative model — versioned snapshot, latest-wins, TOFU keys + +Each `XGrpRoster` is the complete current privileged set. Recipients treat the highest-version valid roster as authoritative for *who is privileged and their keys*; absence from the newest accepted roster means *not privileged* (reverts to the joiner default unless an accompanying `XGrpMemRole` sets a specific role — see 1.6). This is self-healing: a member who missed one change gets the full current state on the next roster. + +**Key handling is trust-on-first-use, pinned per `memberId`** (per entry): + +- `memberId` unknown, or known without a key → store the key (first sight, from the owner). Set name/role. +- `memberId` already has a key: + - same key → fine; update name/role. + - **different key → error.** Never overwrite; keep the old key; surface a suspicious-roster event. + +There is **no in-place key rotation**: a genuine re-key is modeled as a *new member* — the owner removes the old `memberId` (`XGrpMemDel` + roster drop) and adds a new `memberId` with the new key. Consistent with SimpleX's no-mutable-identity stance, and with the `xGrpMemNew` rule in 1.5. + +**Anti-replay / rollback.** The relay cannot forge a signed roster but can replay an older one. + +- The current roster `version` is anchored in the owner-controlled link mutable data (which already holds `OwnerAuth`, profile, subscriber count). The relay cannot forge it. A roster change that bumps the version also updates link data. **Status:** the write side is implemented; the join-time **read/detect** is deferred — comparing the anchor against the relay-served roster at join is racy (the forwarded roster may not have arrived yet → false positives), so correct staleness detection must be triggered by roster arrival, not at join. The residual new-joiner rollback gap below stands; the hard anti-replay (member + relay) is in place. +- **Existing members** reject any roster with `version` below the highest already accepted — full anti-replay for them. +- **New joiners** process the latest version the relay actually serves, even if it lags the link anchor, so honest relay propagation lag never blocks a join. The anchor is used for staleness *detection*, not a hard gate, in v1. +- **Documented residual gap:** a stale/malicious relay can serve an old-but-valid roster to a brand-new joiner. Documented in `channels-overview.md` with future mitigations: escalate verification to an owner, or have the client compare the relay's version to the link anchor and refuse/retry above a staleness threshold. + +## 1.4 Cap on the privileged set + +Bound the privileged set so the signed roster always fits one message — never paginate. A hard **cap on moderators + admins** (owners are on the link, not counted), enforced **at promotion time** on the owner: refuse to elevate beyond the cap with a clear error, so the roster is always constructible as one signed message. + +Derive the number from the single encoded-message budget (verify the exact constant — the encoded-message-length limit minus signature + JSON overhead) divided by worst-case entry size. With `{memberId, name, key, role}` entries this is comfortably in the tens-to-~100 range; pick the final value from the measured worst-case entry. + +## 1.5 Remove the dissemination gates; gate on the roster instead + +Because the relay forwards the roster on join **before anything else**, a privileged member's key/role is owner-established before any relay-asserted introduction arrives. So the relay may now disseminate privileged members' full profiles like any other member, and the gates come out: + +- **Remove** the `when (memRole > GRMember)` throw in `xGrpMemNew` (`Subscriber.hs:2957`). +- **Remove** the forward-side `memberRole' s <= GRMember` filter in `sendBodyToMembers` (confirm exact site in `Subscriber.hs`). +- **Replace** with a roster check in `xGrpMemNew`: for an announcement of a privileged role, require that a member record with that `memberId` already exists **with that privileged role** (roster-established). If found → accept the **profile** update only; **never overwrite `key`, `memberId`, or `role`** (roster-authoritative). If not found → reject (a relay conjuring a privileged member not in the roster). + +`introduceInChannel` forwards the cached roster to the new member first, then proceeds; it may still announce the newcomer to moderators and maintain the relations vector. The per-mod `XGrpMemIntro` carrying keys is no longer the trust path for privileged members. + +## 1.6 Delivery — what is sent, when, to whom + +Two orthogonal axes, and the roster owns only one: + +- **Axis A — privileged set + keys** (who is mod/admin, their key/name/role): owned by the roster. +- **Axis B — group-membership lifecycle** (removed / restricted / left): owned by `XGrpMemDel` / `XGrpMemRestrict` / `XGrpLeave`, unchanged, applies to everyone. + +Dispatch by whether an operation touches the {moderator, admin} set (the *roster roles*). Owner is not a roster role — promotion to/from owner uses `XGrpMemRole` (+ link `OwnerAuth`), never the roster. + +**`APIMembersRole` (role change, possibly batched):** + +- Emit `XGrpMemRole(M, target)` for each affected member exactly as today — this conveys the exact target role for any role, including owner and specific ≤member roles. +- **Additionally** build and **broadcast** the full signed roster (version++) **iff the {mod, admin} set changed** (any member entered, left, or moved within mod/admin). +- A mixed batch fires both. Example: target=member over `[moderator M, observer O]` → `XGrpMemRole` for both (exact roles) **and** a roster (M left the set). The promotion case `XGrpMemRole(M, mod)` + roster is mildly redundant on the role field and harmless; the key comes only from the roster. + +The broadcast reuses the existing owner-admin-event forwarding (`shouldForward = isUserGrpFwdRelay gInfo && not forwarded`, `Subscriber.hs:3191`). Privileged-set changes are rare administrative events, so this is on the order of an `XGrpInfo`/`XGrpPrefs` broadcast — not per-message. The broadcast roster is the **self-healing** mechanism: a member who missed a prior `XGrpMemRole` is corrected by the snapshot. + +**`XGrpMemDel` (removal):** broadcast `XGrpMemDel` as today (it neutralizes the member for existing members). If the removed member was privileged, the owner sends a refreshed roster (version++), which the relay broadcasts like any other version bump (see below). + +**Relay broadcast rule — always broadcast on a strict version bump.** A newer-version roster is applied, cached, and broadcast to current members, uniformly — promotion, key/role change, demotion, or privileged removal. We do **not** try to make removal cache-only: a demotion (member stays in the group) is indistinguishable from a deletion at the roster-diff level, so suppressing the broadcast would silently drop the self-healing the spec requires for role changes. The only cost is one redundant roster broadcast alongside `XGrpMemDel` on the rare deletion of a privileged member — and even there the broadcast is not waste, since it self-heals the privileged-set side if the `XGrpMemDel` was lost. (This supersedes an earlier "cache-only on deletion" idea, which could not be implemented without either a wire flag or a fragile demotion-vs-deletion diff.) + +**On relay add:** the owner sends the current roster to the new relay so it can serve joiners. + +**Joiners:** the relay forwards the cached roster at join (1.5). + +**Short offline gaps** are covered by ordinary queued delivery: the role-change roster broadcast sits in the member's SMP queue (FIFO) ahead of any later moderator events, so it is processed first on reconnect. + +**Quota-blocked catch-up.** A member offline long enough to fill its queue causes the relay to be quota-blocked — the broadcast may never have been enqueued, so naive queueing would leave the member without the current roster, rejecting moderator events indefinitely. Fix: when the queue drains, the relay **sends the current cached roster ahead of the resumed backlog**, so the member holds the current privileged set before processing the events it couldn't verify. + +The hook is confirmed: QCONT is delivered to the **sender** when the recipient drains (`simplexmq Agent.hs:3402`), and the relay receives it per subscriber in the group-member connection handler (`Subscriber.hs:1215` — `continueSending` + `sendPendingGroupMessages user gInfo m conn`, with `gInfo`/`m`/`conn` in scope; a relay→subscriber connection is a group-member connection). Implementation must ensure **roster-first ordering** relative to both the agent-level `continueSending` flush and the re-driven delivery tasks, and gate the extra send on a per-member "delivered roster version" so it fires only when the member is behind. + +The roster is **never** delivered through the profile-dissemination prepend. That path (`member_relations_vector` → `XGrpMemNew`) carries **profiles** only; with the gate removed (1.5), a privileged member's profile disseminates through it like any other member's, but only after the roster has established their key/role. Profile via prepend, key/role via roster — orthogonal, no double-prepend. + +## 1.7 Relay-side cache (the one new storage pattern) + +Relays already forward signed bytes verbatim (`encodeFwdElement` `Batch.hs:106`, `verifiedMsgParts` `Protocol.hs:1445`; `messages.msg_chat_binding` + `msg_signatures`; reconstructed in `toTask` `Delivery.hs:154`). What does **not** exist is "store the latest roster and re-emit to joiners" — `sendHistory` (`Internal.hs:1207`) reconstructs content and does *not* preserve signatures, so it is not a template. + +Add a small per-group cache holding the latest signed roster message bytes, plus the roster `version` as a **separate column** alongside them (so the relay compares versions without re-parsing the blob). On receiving `XGrpRoster` from an owner the relay: verifies the owner signature; **checks `version` strictly greater than the cached version** (lower → reject as rollback; equal → idempotent no-op); then updates its own member-role records, overwrites the cache + stored version, and (for a role-change-origin roster) creates a delivery task to all current members. On join, it forwards the cached bytes verbatim. + +The relay-side version check protects an **honest** relay's cache from being rolled back by a replayed signed roster — which in turn protects every joiner that relay serves. It does not constrain a **malicious** relay (it controls its own cache); that remains the documented new-joiner residual gap (1.3), bounded by the member-side check (1.3) and the link version anchor. + +## 1.8 Races and tests + +- **Promotion vs. action ordering.** A newly-promoted mod could act before its roster reaches a recipient ⇒ `RGEMsgBadSignature`. Covered for MVP by causal ordering (the roster is broadcast at promotion, before the mod learns of and acts on it), QCONT catch-up (item 7), and recipient tolerance (a rejected first action is re-sent; the next roster repairs trust). The fuller fix — the **roster-specific prepend** (prepend the cached signed roster ahead of a privileged member's forwarded action for recipients below the current version, reusing the item-7 delivered-version tracker, distinct from the `XGrpMemNew` profile prepend) — touches the hot per-recipient delivery loop that carries every forwarded message, so it is **deferred to a focused, separately-tested pass** rather than shipped untested. +- **Multi-owner roster signed by an unknown owner** (owner added after the recipient fetched the link): recipient cannot verify ⇒ buffer/refetch link. Cannot occur for single-owner MVP; flag for v7. +- **Roster vs. profile-update concurrency:** benign (different fields); verify the roster's relations-vector handling does not clobber the profile `MRIntroduced` semantics they share. + +Tests: relay-fabricated moderator key is rejected (forgery); promotion delivers a verifiable key; demotion via roster + `XGrpMemRole` reconciles; removed privileged member does not reappear for a new joiner; replayed older roster rejected by existing members; TOFU key-change rejected; batch `APIMembersRole` emits roster + `XGrpMemRole` correctly; self-healing after a dropped role event. + +## 1.9 Key anchors for Section 1 + +`ChatMsgEvent` `Protocol.hs:422`; `requiresSignature` `Protocol.hs:1231`; `groupMsgSigning` `Internal.hs:1962`; `withVerifiedMsg` `Subscriber.hs:3461`; `xGrpMemNew` `Subscriber.hs:2957`; `xGrpMemIntro` `Subscriber.hs:3015`; `introduceInChannel` `Internal.hs:1165`; `getGroupModerators` `Store/Groups.hs:1190`; `memberInfo` `Internal.hs:1187`; `createLinkOwnerMember` `Store/Groups.hs:3072`; `GroupKeys` `Types.hs:462`; `member_relations_vector` machinery in `Types/MemberRelations.hs` (`MemberRelation`, `MRIntroduced`, `IDSubjectIntroduced`, `setNewRelations`); forwarding `Batch.hs:106` / `Protocol.hs:1445` / `Delivery.hs:154`. New columns go in a **new tail migration** (`M20260222_chat_relays` is the *pattern* for relay group columns but is not the tail — never edit an applied migration; `M20260525_member_removed_at` is the current tail). + +--- + +# Section 2 — Joiner role on the signed profile + +Today the relay derives a joiner's role from `channelSubscriberRole` (`Controller.hs:161`, default `GRObserver` `Chat.hs:119`), a global config — so relays can disagree and the owner cannot set it per group. Move it onto the owner-signed profile. + +## 2.1 Types and helpers + +- Add `joinerRole :: Maybe GroupMemberRole` to `PublicGroupProfile` (`Types.hs:798`). No migration: JSON derives via `deriveJSON defaultJSON` with `omitNothingFields = True`, so `Nothing` is omitted on encode and a missing field decodes to `Nothing`. +- Add a `groupType` accessor and `isChannel` on `GroupInfo`/`GroupProfile`, and a resolver `joinerRoleFor :: GroupInfo -> GroupMemberRole` = `joinerRole` if set, else type-keyed default (`GTChannel → GRObserver`, `GTGroup → GRMember`, `GTUnknown _ → GRObserver`). Reuse the existing `publicGroupEditor`/`memberRole'` (`Types.hs:499`/`506`); do **not** introduce a profile-side `memberRole'` (name collision). + +## 2.2 Replace the global config + +Switch every `channelSubscriberRole` reader to `joinerRoleFor gInfo` and delete the config: `Controller.hs:161`, `Chat.hs:119`, `Commands.hs:2053`, `Commands.hs:2546`, `Subscriber.hs:3248`, `Subscriber.hs:4019`. Verify no out-of-tree consumer reads it. + +## 2.3 Command, preferences, defensive refusal + +- `APINewPublicGroup` (`Controller.hs:526`, handler `Commands.hs:2495`) gains `groupType` (default `GTChannel`) and optional `joinerRole` (default `joinerRoleFor` of the type); both written onto the constructed profile (today hardcodes `groupType = GTChannel` at `Commands.hs:2538`). +- Parameterize the channel-prefs parser by `GroupType`: Channel keeps its override (`support = OFF`); Public group and `GTUnknown` use secret-group defaults (member-to-moderator escalation is expected). Do not duplicate the parser — parameterize it. +- `directMessages` stays ON by inheritance but is dormant in any relay-mediated group; hide its toggle when `useRelays` and refuse `xGrpDirectInv` defensively when `useRelays'` (`Subscriber.hs:3321`, currently ungated): emit `messageError`, create no contact. + +## 2.4 Compatibility + +Existing channels: no `joinerRole` ⇒ falls back to `GRObserver` for `GTChannel`. No data migration. Older relays without this change resolve the joiner role from their global config — warn the owner at create time if a selected relay's chat version is below the public-groups version (soft warning, not a block). + +--- + +# Section 3 — Backend tests + +Public-group helpers paralleling the channel helpers, plus: + +1. Member posts; all members receive it (no "unknown member" lines). 2. Multi-author session: no "unknown member" anywhere. 3. Member edit/delete/react forwarded to all. 4. `xGrpDirectInv` refused under `useRelays` (no contact created); repeat for Channel. 5. Blocked member's messages not forwarded. 6. Multi-relay delivery with cross-relay dedup. 7. History on join. 8. `asGroup=true` from a non-owner rejected. 9. Receipts disabled above the member limit. 10. Older client refuses `groupType = "group"` (needs-newer-version). 11. Incognito member posting attributes the incognito profile. 12. `joinerRole` propagates and defaults correctly (Channel→observer, Public group→member; absent→type default). Plus the roster tests in 1.8. + +--- + +# Section 4 — Clients (iOS, then Kotlin) + +## 4.1 Audit `useRelays` vs `isChannel` (structural commit, on its own) + +~70–75 sites per platform branch on `useRelays` as a proxy for "is a channel." Split per a mechanical rule and land as a pure structural commit (no behavior change in the same diff): + +- **Transport** (keep `useRelays`): link/relay management, owner-can't-leave-own-relay-group, relay-status indicator, incognito flag, typing-state gating, member-DM-affordance suppression. +- **Governance** (switch to `isChannel`): titles, "subscribers" vs "members" framing, "Channel preferences" labels, channel-style vs group-style member display. + +## 4.2 Model and behavior + +- Model: add `group` arm to `GroupType` (with serializers); `joinerRole`, `groupType`, `isChannel` accessors. Authoritative role resolution stays in Haskell; clients use it for display. +- **Narrow the existing refusal:** PR #7009 (merged to `stable`) added `GLPUpdateRequired` for `groupType /= GTChannel` (`Controller.hs:1051`, `Commands.hs` `unsupportedGroupType`). Change it to refuse only `GTUnknown _`; `GTGroup` proceeds to a public-group join. +- Create flow: one view with a Channel / Public-group segmented control (default Channel) driving the title, link-step label, success screen, and two API params (`groupType`, `joinerRole` = observer for Channel, member for Public group — no role picker in MVP). Hide `directMessages` in create prefs when `useRelays`. Render the threat-model note below the title for Public groups (text in Section 5). +- Suppress the member-tap "send direct message" affordance in any relay-mediated group. +- Members view shows the relay-known roster; header "subscribers" (channel) vs "members" (public group). No filtered view in MVP. +- Strings/icons: ~5–10 `_public_group` string keys mirroring channel forms; reuse `group_members_*` for "members" framing; a distinct Public-group icon (pending design). Kotlin-only: chat-list filter chips place Public groups in the "groups" bucket. + +Platforms ship independently (API defaults are backward compatible). + +--- + +# Section 5 — Threat model, docs, release + +Fold into `channels-overview.md` (public groups inherit the entire channel threat model; deltas only): + +- **A relay can fabricate content as any member** (channels: only as owners). Content (`XMsgNew`/`Update`/`Del`/`React`) is unsigned by design for deniability (`requiresSignature` lists roster/admin events only); broader blast radius in public groups. Detectable via cross-relay consistency. Mitigation is the future opt-in content signing on the channel roadmap; the create-flow note states the trade-off ("a malicious relay could change or fabricate messages from any member — pick relays you trust, or use a secret group for peer-to-peer integrity"). +- **Roster rollback for new joiners** (1.3): documented bounded delta + future mitigations. +- Unchanged: relay cannot impersonate an owner or substitute the profile (signed events, validated entity ID); `joinerRole` and the privileged roster are owner-signed, so the relay cannot unilaterally change a joiner's default role or fabricate a moderator. + +Document `XGrpRoster` (event, signing, versioning, TOFU, delivery) in `channels-protocol.md`. Bump the chat protocol version (the public-groups version that gates `GTGroup` and `joinerRole`). Release notes include the relay-fabrication line. + +--- + +# Sequencing + +1. **Section 1 — privileged roster** (backend). The core; gates the rest of the value. Land the gate-removal/roster-check and the event together so no half-applied trust window exists. +2. **Section 2 — joiner role on profile** (backend). Independent of Section 1. +3. **Section 3 — backend tests** (alongside 1–2). +4. **Section 4 — clients**: audit (structural) first, then iOS, then Kotlin. +5. **Section 5 — docs/version/release** with the backend release. + +Backend (1–3) gates the clients. iOS and Kotlin are independent of each other. + +--- + +# Out of scope (deferred) + +- **Member-to-member DMs in relay-mediated groups.** Prohibited here (client affordance suppressed, receive-path refusal, relay does not forward `XGrpDirectInv` — so no relay-visible DM graph). A future plan must re-derive the threat model: relay-forwarded DMs would expose (sender, target, time) metadata; relay-blind rendezvous via per-member queues is the privacy-preserving alternative. +- **`memberAdmission` on relay-mediated join** (hardcoded `GAAccepted` bypasses review/captcha — generic relay-groups gap). +- **Roster filter/pagination in the members view** for very large groups. +- **Multi-owner** roster signing/verification and owner promotion via link `OwnerAuth` (v7); **opt-in content signing** (v7 roadmap); **full anti-rollback for new joiners** (link-version hard gate). diff --git a/plans/2026-06-01-roster-members-multipart.md b/plans/2026-06-01-roster-members-multipart.md new file mode 100644 index 0000000000..aca98c4698 --- /dev/null +++ b/plans/2026-06-01-roster-members-multipart.md @@ -0,0 +1,220 @@ +# Roster: regular members + larger rosters via inline file + +Date: 2026-06-01 (revised). Extends Section 1 of `2026-05-26-public-groups-via-relays-unified.md`. + +> Anchors below were re-verified against `f/public-groups-members-in-roster` **after** PR #7036 (`core: signed XMember in public group`, commit `0773ccd05`) merged in. Most line numbers shifted; the header-fits check uses `maxEncodedMsgLength = 15602` (now `Protocol.hs:905`). Confirm before editing. + +## Reconciliation with PR #7036 (merged into this branch) + +PR #7036 landed things this plan predates. Read this section first — it changes the relay flow the plan builds on. + +**Renames (the plan's old names no longer exist — grep will miss them):** + +| Was | Now | Location | +|---|---|---| +| `forwardCachedRoster` | `forwardGroupRoster` | `Internal.hs:1172` | +| `setCachedGroupRoster` | `setGroupRoster` | `Store/Groups.hs:1415` | +| `getCachedGroupRoster` | `getGroupRoster` | `Store/Groups.hs:1428` | +| `setRelayLinkAccepted` | `setRelayKey` (no longer sets relay status) | `Store/Groups.hs:1543` | + +"Cached roster" is now "saved roster" throughout; the `roster_msg_*` columns and the `roster_blob` this plan adds are unchanged in intent. + +**Roster version baseline is now `Just 0`, not NULL, for relay groups.** `createNewGroup` initializes `roster_version = Just (VersionRoster 0)` for `useRelays` groups (`Store/Groups.hs:365, 427`), and an old channel materializes `0` the first time a relay connects (`Subscriber.hs:905-910`). Consequences: the first promotion bumps `0 -> 1` (not NULL -> 0); the owner and already-onboarded members/relays compare against a real `0`, **but a relay's own `roster_version` is still NULL the first time it receives v0** — applying v0 from NULL is exactly what lets it ack and become publishable (verified: today's `maybe False (newVer <=) (rosterVersion gInfo)` at `Subscriber.hs:3207` applies v0 only because the relay is at `Nothing`; `Just 0` would reject it and the relay would hang `RSInvited`). So the multipart version guards MUST be `Maybe`-comparisons that treat `Nothing` as below `0` (spelled out under *Header handler* and *Completion*); and the **empty roster (v0)** must round-trip through the header+blob path (a 2-byte blob: `Word16` count `0`, one chunk, `chunkSize >= fileSize` -> `RcvChunkFinal` on chunk 1). The empty/small blob is the *common* case for relay onboarding, not an edge case. + +**NEW relay roster-ack handshake (`XGrpRosterAck`) — this plan MUST integrate it.** PR #7036 added `XGrpRosterAck :: VersionRoster -> Maybe Text` (`Protocol.hs:499`; tag `x.grp.roster.ack`; NOT in `requiresSignature` — it rides the relay's authenticated connection). Flow: + +- On relay connect (`GCInviteeMember` + `isRelay`) the owner **always** sends the current roster via `sendGroupRosterToRelay`, and the relay stays `RSInvited` (**unpublishable**) until it acks (`Subscriber.hs:900-910`). +- The relay applies the roster in `relayApplyRoster` and, **only while its own status is `RSAccepted`**, sends `XGrpRosterAck author newVer Nothing` (or an error string) — `Subscriber.hs:3210-3221`, `sendRosterAck` at `3276`. +- The owner's `xGrpRosterAck` handler (`Subscriber.hs:3279-3297`) transitions the relay `RSInvited -> RSAccepted` (and publishes via `setGroupLinkDataAsync`) on a version-matching success ack, or `RSInvited -> RSRejected` on error. + +Impact on the multipart design (a REQUIRED change, not just a rename): under this plan the header only *starts* a transfer, so on a relay the **apply, `setGroupRoster`, the ack, AND the broadcast all move to blob completion**, not header receipt. The relay becoming publishable now **gates on the blob transfer completing**: header -> chunks -> verify digest -> `processRoster` + `setGroupRoster` -> (`relayOwnStatus == RSAccepted`) `sendRosterAck` -> broadcast. This is the desired fail-safe (an unpublishable relay can't serve a half-applied roster), but it puts owner->relay blob delivery on the relay-onboarding critical path, not just self-healing. The error branch MUST still ack-with-error (digest mismatch / parse failure -> `sendRosterAck author newVer (Just "...")`) so the owner marks the relay `RSRejected` instead of leaving it hung at `RSInvited`. The current `relayApplyRoster` to fork is `tryAllErrors (setRoster sm)`, where `setRoster` = `processRoster` + `setGroupRoster` (`Subscriber.hs:3205-3228`); the multipart version splits this across header (write `roster_pending_*`) and completion (run `setRoster` + ack + broadcast). + +## Goal + +Let owners promote channel subscribers to **regular members** who can post, and carry more named members than fit one message. + +The JSON roster already exists (event, signing, relay cache, TOFU apply, broadcast, join forward, QCONT). This plan **widens the roster set to include plain members and changes the delivery** to a binary blob over the inline file transfer; the apply logic (`processRoster`) is reused. + +The member list moves out of the `XGrpRoster` message into a binary blob sent over the existing inline file transfer. `XGrpRoster` becomes a small signed header (version + the blob's size and digest). + +## Roster set: the promoted set {member, mod, admin} + +Owners stay on the link, never in the roster. Two edits, then every gate follows. + +**1. Widen `isRosterRole`** (`Internal.hs:1237`) to `{GRMember, GRModerator, GRAdmin}`. Every call site wants the promoted set, so this single edit covers: + +- `validateGroupRoster` filter (`Internal.hs:1243`) — fixes the bug where member entries are dropped. +- `buildGroupRoster` filter (`Internal.hs:1255`). +- promotion gates / cap / trigger / counts (`Commands.hs:2737, 2739, 2746, 2762, 2763, 2768`); update the cap error text at `2740`. +- owner-remove roster refresh (`Commands.hs:2888`, guarded by `anyPrivilegedRemoved` computed from `isRosterRole` at `2899`) — so removing a plain member, not just a mod/admin, refreshes the roster. +- receive gates: `xGrpMemNew` (`Subscriber.hs:3011/3026/3045`) and `xGrpMemRole` owner-only (`3185`). +- **join key-proof gate (NEW in PR #7036, `Subscriber.hs:1620`, `memberJoinRequestViaRelay`)**: a join claiming a `memberId` already roster-established as `isRosterRole` must prove possession of the pinned key (signature + `memberPubKey` match + `viaRelay == this relay's memberId`). Widening to members **extends this proof to promoted members** — a promoted member re-connecting through a relay must sign its `XMember` with the roster-pinned key. This is correct and desirable (it is the receive-side counterpart of the promote-time key invariant in *Known limitations*), and it composes with PR #7036's `acceptGroupJoinRequestAsync existingMem_` path that attaches the connection to the existing roster record. Confirm the promoted member signs its join `XMember` (it does — `encodeXMemberConnInfo`, `Internal.hs`). + +**2. Split the role query.** `getGroupRosterMembers` (`Store/Groups.hs:1215`) currently serves two now-diverging needs: + +- **Build / revert** wants the promoted set. Redefine `getGroupRosterMembers` to `member_role IN (GRMember, GRModerator, GRAdmin)` (current members). Callers: `bumpAndBroadcastRoster` (`Internal.hs:2175`), `sendGroupRosterToRelay` (`2188`), and the `processRoster` revert set `currentPriv` (`Subscriber.hs:3245`). Build and revert MUST be the same query, or a dropped member is never reverted. +- **`introduceInChannel`** (`Internal.hs:1188`) wants only the moderation set (mod+admin). Widening it would announce every joiner to every member and introduce every member to every joiner (traffic + anonymity blowup). Reuse the existing `getGroupModerators` (`Store/Groups.hs:1204-1209`, returns mod+admin+owner) rather than adding a function: keep `getGroupOwners` for the owner-first intro, and take mod+admin as `getGroupModerators` minus owners. **Re-apply `filter memberCurrent`** — `getGroupModerators` does NOT filter current members (unlike the old `getGroupRosterMembers`), so without it a removed or left moderator would be introduced to joiners. Members are learned from the roster blob, not introductions. + +**Owner-only (confirmed decision).** Only the owner changes any roster role. The alternatives were considered and rejected for v1 — letting a mod/admin set member roles would need either the owner co-signing rosters from a mod/admin (owner round-trip + load) or a separate roster-signing key trusted from mod/admin (broader trust surface) — so owner-only keeps the single owner-key trust anchor. + +**Leave and owner-remove differ.** This plan **removes** the `xGrpLeave` roster-bump block (`Subscriber.hs:3439`): since `isRosterRole` is widened, it would otherwise fire `bumpAndBroadcastRoster` on every plain-member leave. So a member **leave** does NOT bump the roster — the leave is the membership axis (`XGrpLeave` neutralizes the member on the relay). An owner **remove** (`APIRemoveMembers`) DOES still bump via `bumpAndBroadcastRoster` (`Commands.hs:2888`, widened to cover plain members). `bumpAndBroadcastRoster` thus stays only for promotion (`APIMembersRole`) and owner-remove. + +## Wire: signed header + unsigned blob + +**Authoritative metadata is in the signed header.** `version`, blob `fileSize`, and `fileDigest` all live in the owner-signed `XGrpRoster`; the unsigned `BFileChunk`s carry no authoritative metadata. (This is why "total parts in the unsigned part", an earlier review question, is a non-issue here.) + +- **Header**: `XGrpRoster { version :: VersionRoster, fileInv :: InlineFileInvitation }`, JSON, signed, forwarded. `InlineFileInvitation { fileSize, fileDigest :: FD.FileDigest }` is a lean `FileInvitation` (no name/connReq/inline/descr; always inline). Tiny; fits `maxEncodedMsgLength` (15602, `Protocol.hs:905`). +- **Blob**: the binary member list. `RosterMember { memberId, key, role, privileges :: Word16 }` — drop `name`, add `privileges` (reserved: always `0`, parsed and ignored in v1). ~60 B/entry. Members get a placeholder name from `nameFromMemberId`; real profiles arrive on first post. +- **Serializer/parser**: a binary codec for the blob (a `Word16`-count-prefixed `[RosterMember]`); `RosterMember` becomes binary-only. Full code in *Blob format* below. Owner serializes → digest → chunks; receiver concatenates chunk bytes → verifies the digest → parses. +- **Cap** `maxGroupRosterSize` → **256** (tunable). Enforce at promotion over the promoted set (`Commands.hs:2739`, via the widened predicate); the receive-side entry-count bound is the parser alone (`rosterBlobP`'s `n > maxGroupRosterSize`); reject a signed `fileSize > cap × max-entry-size` before creating a file. Roster files are exempt from the inline `offer/receiveChunks` ceiling (at 256 ≈ 15 KB the blob is about one `fileChunkSize` chunk; the multipart path handles two if role words push it over). + +**Type changes.** + +- `GroupRoster` (`Protocol.hs:372-376`): `{version, roster :: [RosterMember]}` → `{version, fileInv :: InlineFileInvitation}`. It stays JSON (the signed header), so `InlineFileInvitation` needs a JSON instance; update its stale doc comment ("Owner-signed snapshot of the privileged (moderator/admin) set"). +- `RosterMember` (`Protocol.hs:378`): drop `name`, add `privileges :: Word16`; remove `deriveJSON` (`Protocol.hs:812`) — binary-only now — and add the `Encoding` below. `buildGroupRoster`'s constructor (`Internal.hs:1255`, currently `name = memberShortenedName m`) drops the `name` field; the consumer side already maps to `nameFromMemberId`. +- `validateGroupRoster` (`Internal.hs:1241-1242`): was `GroupRoster -> GroupRoster` over `.roster`; now `[RosterMember] -> [RosterMember]`, run on the parsed blob. + +### Blob format (serializer / parser) + +`RosterMember` is **binary-only** (carried in the blob, never in a JSON message) and gets the `Encoding` below. `MemberKey` (`Types.hs:972`, only `StrEncoding`) and `GroupMemberRole` (`Types/Shared.hs:33`, only `TextEncoding`) lack a binary `Encoding`: `MemberKey` delegates to the underlying `PublicKey` (`Crypto.hs:568`), and the role delegates to its canonical `TextEncoding` (the same `"member"/"moderator"/"admin"` form JSON and the DB use — single source of truth; `GRUnknown` round-trips). + +```haskell +-- MemberKey gains a binary Encoding (it only had StrEncoding); delegate to the Ed25519 key. +instance Encoding MemberKey where + smpEncode (MemberKey k) = smpEncode k + smpP = MemberKey <$> smpP + +-- General instance (belongs beside GroupMemberRole's TextEncoding in Types/Shared.hs, not here). +instance Encoding GroupMemberRole where + smpEncode = smpEncode . textEncode + smpP = maybe (fail "bad GroupMemberRole") pure . textDecode =<< smpP + +-- Tuple encoding (Encoding (a,b,c,d), Encoding.hs:192), as GrpMsgForward / FwdSender do. +instance Encoding RosterMember where + smpEncode RosterMember {memberId, key, role, privileges} = smpEncode (memberId, key, role, privileges) + smpP = RosterMember <$> smpP <*> smpP <*> smpP <*> smpP + +-- Blob = Word16 count (NOT smpEncodeList: its 1-byte count overflows at the 256 cap) followed +-- by that many entries. This is the byte sequence the digest is computed over and verified +-- against before parsing. +encodeRosterBlob :: [RosterMember] -> ByteString +encodeRosterBlob ms = smpEncode (fromIntegral (length ms) :: Word16) <> B.concat (map smpEncode ms) + +rosterBlobP :: Parser [RosterMember] +rosterBlobP = do + n <- fromIntegral <$> smpP @Word16 + when (n > maxGroupRosterSize) $ fail "roster: too many entries" + A.count n smpP +``` + +- **Owner**: `encodeRosterBlob` over the promoted set → `FileDigest` (SHA-512, as the file machinery computes it, `LC.sha512Hash`) → chunk; the digest goes in the signed `XGrpRoster` header. +- **Receiver**: concatenate chunk bytes → verify the digest (S1, over plaintext) → `parseAll rosterBlobP` (consume all input; reject trailing bytes). Parsing runs only after the digest matches, so the bytes are owner-attested; the `n > maxGroupRosterSize` guard and `parseAll` are defensive against a buggy/garbled blob. +- **Per-entry layout**: `memberId` (1-byte len + id) + `key` (1-byte len + Ed25519 pubkey) + role (1-byte len + role word, e.g. `member` = 7 B) + `privileges` (2 bytes) ≈ ~60 B/entry. The file-transferred blob has no tight size budget, so canonical text is fine. +- `privileges` is reserved: serialized as `0`, parsed and ignored in v1. + +## Delivery: send → header → chunks → completion + +### Owner send + +`bumpAndBroadcastRoster` and `sendGroupRosterToRelay` build the blob (`buildGroupRoster` over the widened query), compute its `FileDigest`, send the `XGrpRoster` header, then send the blob as `BFileChunk`s against that message's `shared_msg_id`. + +`sendFileInline_` reads from a file, so add a send-from-bytes variant (shared with the relay re-serve). The owner's own version bump stays as today (in `bumpAndBroadcastRoster`, `Internal.hs:2176`) — the owner is the source of truth; "bump only at completion" is a receive-side rule. + +### Header handler (`xGrpRoster`, member and relay) + +The header no longer applies anything — it starts a transfer. It only writes `roster_pending_*`; it never writes `roster_version` or the live `roster_msg_*`. + +- **Short-circuit** unless `version > max(roster_version, roster_pending_version)` — strictly greater than both applied and pending — before creating a file. These are **`Maybe` comparisons: `Nothing` (un-materialized version) counts as below `0`** (mirror today's `maybe False (newVer <=) …`, `Subscriber.hs:3207`), so a relay's first receipt at NULL **applies** v0 while a re-receive at `Just 0` short-circuits — the v0 onboarding depends on this. + - Why both: the QCONT re-serve is unconditional, so the relay may re-forward a still-cached v5 while a member is mid-receiving v6 (applied 4). Compared only to applied, v5 > 4 would supersede v6, then the arriving v6 chunks fail the v5 digest → stuck. + - Why never bump here: a header-time bump makes the genuine blob complete as an equal-version no-op, leaving the receiver at `vN` with `v(N-1)` data. +- **Create the rcv-file** with `cryptoArgs = Nothing` (see Security), `file_type = roster`, `chat_item_id` NULL, `shared_msg_id` = the header's id. Accept it via `startRcvInlineFT` (chat-item-free), not `acceptRcvInlineFT`, so chunk 1 isn't rejected on `RFSNew`. +- **One in-flight per group is automatic**: the single `groups` row makes `roster_pending_*` single-valued, and there is one `(group_id, file_type = roster)` file. A duplicate header is idempotent. A version greater than both applied and pending supersedes: `UPDATE roster_pending_*` and delete the existing roster file (cleanup below), then create the new. + +### Chunks + +The header is enqueued before chunk 1 (per-connection FIFO). + +**Reset-on-chunk-1** (decision 4): if chunk 1 arrives with partial chunks, discard and restart so relay restart / re-subscribe / QCONT can re-drive from the start. Discarding MUST (GAP 3): + +- delete the `rcv_file_chunks` rows, +- truncate/remove the on-disk file, and +- evict its handle from the `rcvFiles` map (`closeFileHandle`). + +`appendFileChunk` opens in AppendMode and caches the handle (`Internal.hs:1781`, handle at `1794`), so clearing only the rows would append after the stale bytes and corrupt the blob (digest fails — the stuck state decision 4 avoids). + +**Orphaned chunk**: a roster `BFileChunk` matching no in-flight `(group_id, shared_msg_id, file_type = roster)` file is **ACKed and ignored**, never errored (the version is already applied or superseded). This is how an up-to-date member tolerates the unconditional re-serve: the re-served header short-circuits (no file), then its chunks arrive with no transfer in flight. Distinct from reset-on-chunk-1, which fires only when partial chunks exist. + +### Completion (on `RcvChunkFinal`) + +1. Verify the assembled file's digest against `roster_pending_digest`. On mismatch, discard (delete the file, clear `roster_pending_*`); do not apply or bump. +2. **Version guard**: apply only if `roster_pending_version > roster_version` (same `Maybe` semantics — a `Nothing` applied version counts as below `0`, so a first v0 completion from NULL applies). A stale/out-of-order completion is rejected, not applied as a downgrade. +3. Parse → `validateGroupRoster` → `processRoster` (TOFU keys, role updates, revert absent promoted members, role-change items; pass `nameFromMemberId` where it used the entry name). + +In **one transaction**: `processRoster` → set `roster_version = roster_pending_version` → set `roster_blob` → clear `roster_pending_*` → delete the file. A **relay** also promotes the pending signed-header columns into the live `roster_msg_*` (this is what `setGroupRoster` writes today at header time — it moves here) and applies to its own records, then **sends the roster ack and broadcasts** (below). So a joiner never sees a live header at `vN` paired with a blob at `vN-1`. + +**Relay ack at completion (PR #7036 integration).** The relay's `XGrpRosterAck` (previously sent in `relayApplyRoster` at header receipt) moves to completion, gated exactly as today on `relayOwnStatus gInfo == Just RSAccepted`: on a successful completion send `sendRosterAck author roster_pending_version Nothing`; on digest-mismatch or parse failure send `sendRosterAck author roster_pending_version (Just "...")` so the owner marks the relay `RSRejected` rather than leaving it `RSInvited` forever. A relay therefore becomes publishable only after the full blob arrives and applies — the desired fail-safe, but it makes owner→relay blob delivery part of the relay-onboarding path, so it MUST be reliably driven (see *Owner send* and *Relay re-serve*; for a freshly-connecting relay the owner drives it via `sendGroupRosterToRelay`, including the empty v0 blob). The `relayOwnStatus == Just RSAccepted` gate is ported unchanged but now evaluated at completion rather than header receipt — confirm `relayOwnStatus` cannot change across the header→completion window (it shouldn't: a relay can't reach `RSActive` before acking, since the ack is what publishes it). + +The version guard plus the per-version `shared_msg_id` keying are what make the design correct; the short-circuit and one-in-flight-per-group are optimizations. + +### Relay re-serve (broadcast / join / QCONT) + +Per recipient, forward the signed header (as `forwardGroupRoster` does today, `Internal.hs:1172`) AND re-send the blob as `BFileChunk`s from `groups.roster_blob` (the send-from-bytes variant). An incoming `BFileChunk` returns no delivery task (`Subscriber.hs:1089`), so the blob send is driven here. + +**No per-member version gate in v1 (GAP 2).** QCONT/SENT re-forwards the saved roster unconditionally today (`Subscriber.hs:1143`, `1237`), and no per-member delivered-version tracker exists in the tree — this plan adds none. So a re-serve re-sends the whole blob on every drain; at cap 256 that is ~15 KB — one (occasionally two) `BFileChunk`s per drain — acceptable. + +It is safe because: an up-to-date member short-circuits the header and ACK-ignores the orphaned chunks; a stale (≤ pending) re-forward mid-transfer is a no-op via the short-circuit; and the completion version guard rejects any stale completion. + +If the cap is later raised so the blob spans many chunks, add a per-member `delivered_roster_version` column (read on QCONT/join/broadcast, written on confirmed delivery) and re-serve only when behind — future work. + +### Supersede / cancel cleanup + +Cleanup spans ALL of these — miss none: + +- `files`, `rcv_files`, `rcv_file_chunks`, +- the on-disk file and its `rcvFiles` handle (`closeFileHandle`), +- the `roster_pending_*` columns on `groups` (set NULL). + +## File-machinery changes (only these) + +- **Lookup**: add `files.shared_msg_id`; resolve roster chunks by `(group_id, shared_msg_id, file_type = roster)`. Leave `getGroupFileIdBySharedMsgId` (`Store/Files.hs:310`, chat-item JOIN) for normal files; branch on `file_type` / `chat_item_id IS NULL`. +- **Fork the three receive sites that call `getChatItemByFileId`** (they throw with no chat item): + - `startReceivingFile` (`Internal.hs:827`, reached on chunk 1) — skip the chat item + `CEvtRcvFileStart`. + - `receiveFileChunk` `RcvChunkFinal` (`Subscriber.hs:1329`) — replace with the completion path above. + - `FileChunkCancel` (`Subscriber.hs:1313`) — delete file + drop in-flight state, no chat item. +- **Cleanup keyed on `group_id`** (not chat items): `getGroupFileInfo` INNER-JOINs `chat_items`, so group delete (`Commands.hs:1270`) and clear (`1305`) skip roster files; the DB row cascades on group delete but the on-disk file leaks. Add a roster-file cleanup for delete/clear, cancel, and supersede. + +## Storage / migration + +In-flight state lives on `groups` (mirroring the live saved roster) and `files` (located by `shared_msg_id`) — no join table. These columns go into the in-progress **`M20260602_group_roster`** migration (already part of this work, not yet merged — so it's editable, not an applied migration), SQLite + Postgres; tests regenerate the schema files. + +| `groups` column(s) | Holds | Lifecycle | +|---|---|---| +| `roster_version` *(kept)* | applied version | bumped at completion | +| `roster_msg_*` *(kept)* | live signed header (was full JSON) | relay forwards verbatim; promoted from pending at completion | +| `roster_blob` *(new)* | durable completed blob | written at completion; relay re-serves it | +| `roster_pending_version`, `roster_pending_digest` *(new)* | in-flight version + digest | set on header receipt; cleared at completion | +| `roster_pending_msg_*` *(new, relay-only)* | in-flight signed header | set on header receipt; promoted to live at completion (NULL on members) | + +The kept `roster_msg_*` columns stay the relay's verbatim-forward source and trust anchor: `forwardGroupRoster` re-forwards them so the joiner verifies the owner signature, and the digest inside authenticates the unsigned blob. + +`files` adds `shared_msg_id` and `file_type`. The in-flight transfer is the `files` / `rcv_files` / `rcv_file_chunks` rows with `(group_id, file_type = roster)`. + +## Security + +- **Owner-signed header**: assert `memberRole' author == GROwner` (`Subscriber.hs:3198`); keep `XGrpRoster_` in `requiresSignature`. +- **Integrity is entirely the digest** (S1): verify the assembled **plaintext** blob against the owner-signed `fileDigest` at completion. Hence `cryptoArgs = Nothing` — a set cryptoArgs makes `appendFileChunk` re-encrypt the file in place (`Internal.hs:1801`), so the on-disk bytes would be ciphertext and the check would fail. A corrupted chunk fails the digest and the roster is rejected. +- **TOFU** key pinning per `memberId` unchanged (different key for a known id → keep the trusted key). +- **Rollback (S2)**: the signature binds `publicGroupId + version` and the digest binds the blob to that header, so cross-group/version substitution stays blocked. But the blob now carries plain members, so a same-group replay of an old `(header, blob)` to a **new joiner** can re-introduce a removed poster or mask a demotion (existing members are protected by the version check). Update `channels-overview.md`. + +## Known limitations / out of scope + +- A malicious relay can withhold/corrupt chunks → the member stays on its last-applied roster (it can drop any message anyway); new-joiner rollback now covers plain members. +- A just-promoted member's first posts may show "unknown member" until the file arrives — self-healing. +- A member who **leaves** lingers in the roster blob until the next bump (this plan drops the leave-triggered refresh). Harmless: they have no relay connection and cannot post, so a new joiner sees only a ghost row; the owner's explicit remove (`APIRemoveMembers`) drops them. +- Out of scope: granting/enforcing `privileges`; member content signing; joiner-role-on-profile; clients. Do not couple the roster set to the joiner-role mechanism (decision 2) — it is the absolute `{member, mod, admin}`. + +## Tests (`tests/ChatTests/Groups.hs`) + +The roster tests now live under the `describe "promoted members roster"` block (PR #7036 moved them and added `testChannelAddRelayWithRoster`, which onboards a 2nd relay through the roster-ack handshake). Update those to header+file delivery — `testChannelAddRelayWithRoster` in particular now exercises the v0/empty-roster blob transfer feeding the relay ack, so it must drive the header+chunk(s) to completion before the relay acks. + +Then add: digest-mismatch blob rejected (no apply, no version bump) **and the relay acks-with-error → owner marks it `RSRejected`** (PR #7036 path); a relay does **not** ack / become publishable until the blob completes (ack moved to completion); member promotion enters the broadcast roster and can post; reset-on-chunk-1 recovery; superseding version cleans up the in-flight older file; version not bumped on header receipt or on a failed blob; `introduceInChannel` still mod+admin only (no member introductions); on-disk roster file cleaned on group delete/clear mid-transfer; non-owner promotion refused; a promoted member re-connecting through a relay is accepted only with a valid signed `XMember` over the roster-pinned key (the widened `memberJoinRequestViaRelay` gate). Existing mod/admin tests must still pass. diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 22b97c83ee..f1effc2e6a 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -143,6 +143,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain + Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster else exposed-modules: Simplex.Chat.Archive @@ -304,6 +305,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain + Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index ca4f3e8fcf..004d790844 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1289,6 +1289,8 @@ processChatCommand cxt nm = \case filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "deleteChat group" chatId $ do deleteCIFiles user filesInfo + -- the roster blob file has no chat item, so it is missed by getGroupFileInfo above + cleanupGroupRosterFile user gInfo (members, recipients) <- getRecipients gInfo let doSendDel = memberActive membership && isOwner msgSigned <- @@ -2050,9 +2052,9 @@ processChatCommand cxt nm = \case gVar <- asks random (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing hostMember <- maybe (throwCmdError "no host member") pure hostMember_ - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let cd = CDGroupRcv gInfo Nothing hostMember - createItem sharedMsgId content = createChatItem user cd True content sharedMsgId Nothing + createItem sharedMsgId content = createChatItem user cd True content sharedMsgId Nothing Nothing cInfo = GroupChat gInfo Nothing void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message @@ -2062,9 +2064,9 @@ processChatCommand cxt nm = \case pure $ CRNewPreparedChat user $ AChat SCTGroup chat ACCL _ (CCLink cReq _) -> do ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId - void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) let cd = CDDirectRcv ct - createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing + createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing Nothing cInfo = DirectChat ct void $ createItem Nothing $ CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ connRequestPQEncryption cReq void $ createFeatureEnabledItems_ user ct @@ -2081,11 +2083,11 @@ processChatCommand cxt nm = \case subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember gVar <- asks random (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let cd = maybe (CDChannelRcv gInfo Nothing) (CDGroupRcv gInfo Nothing) hostMember_ cInfo = GroupChat gInfo Nothing void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo - aci <- forM description $ \descr -> createChatItem user cd True (CIRcvMsgContent $ MCText descr) welcomeSharedMsgId Nothing + aci <- forM description $ \descr -> createChatItem user cd True (CIRcvMsgContent $ MCText descr) welcomeSharedMsgId Nothing Nothing let chat = case aci of Just (AChatItem SCTGroup dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats @@ -2153,7 +2155,7 @@ processChatCommand cxt nm = \case -- create changed feature items (connecting incognito sends default preferences, instead of user preferences) lift . when incognito $ createContactChangedFeatureItems user ct ct' forM_ msg_ $ \(sharedMsgId, mc) -> do - ci <- createChatItem user (CDDirectSnd ct') False (CISndMsgContent mc) (Just sharedMsgId) Nothing + ci <- createChatItem user (CDDirectSnd ct') False (CISndMsgContent mc) (Just sharedMsgId) Nothing Nothing toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToContact user ct' customUserProfile CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct' @@ -2246,7 +2248,7 @@ processChatCommand cxt nm = \case liftIO $ setPreparedGroupStartedConnection db groupId getGroupInfo db cxt user groupId forM_ msg_ $ \(sharedMsgId, mc) -> do - ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing + ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing Nothing toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToGroup user gInfo' customUserProfile [] CVRConnectedContact _ct -> throwChatError $ CEException "contact already exists when connecting to group" @@ -2756,34 +2758,45 @@ processChatCommand cxt nm = \case -- TODO [relays] possible optimization is to read only required members + relays g@(Group gInfo members) <- withFastStore $ \db -> getGroup db cxt user groupId when (selfSelected gInfo) $ throwCmdError "can't change role for self" - let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members + let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending, anyPrivilegedTarget, finalPrivilegedCount) = selectMembers members when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $ throwCmdError "can't change role of multiple members when admins selected, or new role is admin" when anyPending $ throwCmdError "can't change role of members pending approval" assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) + -- in relay groups the roster has a single signer, so only the owner may change moderator/admin roles + when (useRelays' gInfo && (isRosterRole newRole || anyPrivilegedTarget) && memberRole' (membership gInfo) /= GROwner) $ + throwCmdError "only the group owner can change moderator and admin roles" + when (useRelays' gInfo && isRosterRole newRole && finalPrivilegedCount > maxGroupRosterSize) $ + throwCmdError $ "the number of members, moderators and admins would exceed the limit of " <> show maxGroupRosterSize (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems - (errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g currentMems + let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && (isRosterRole newRole || anyPrivilegedTarget) + rosterVer <- if doBumpRoster then Just <$> reserveRosterVersion gInfo else pure Nothing + (errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g rosterVer currentMems + forM_ rosterVer $ \v -> broadcastRoster user gInfo v `catchAllErrors` eToView unless (null acis) $ toView $ CEvtNewChatItems user acis let errs = errs1 <> errs2 unless (null errs) $ toView $ CEvtChatErrors errs pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole, msgSigned} -- same order is not guaranteed where selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds - selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) - selectMembers = foldr' addMember ([], [], [], GRObserver, False, False) + -- anyPrivilegedTarget: a target currently moderator/admin; finalPrivilegedCount: + -- moderators + admins after the change (targets take newRole, others keep their role). + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool, Int) + selectMembers = foldr' addMember ([], [], [], GRObserver, False, False, False, 0) where - addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending) + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, privCount) | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin anyPending' = anyPending || memberPending m - in - if - | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending') - | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending') - | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending') - | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending) + anyPrivTarget' = anyPrivTarget || isRosterRole memberRole + privCount' = if isRosterRole newRole then privCount + 1 else privCount + in if + | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', privCount') + | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', privCount') + | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', privCount') + | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, if isRosterRole memberRole then privCount + 1 else privCount) changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember]) changeRoleInvitedMems user gInfo memsToChange = do -- not batched, as we need to send different invitations to different connections anyway @@ -2798,19 +2811,20 @@ processChatCommand cxt nm = \case withFastStore' $ \db -> updateGroupMemberRole db user m newRole pure (m :: GroupMember) {memberRole = newRole} _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName - changeRoleCurrentMems :: User -> Group -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) - changeRoleCurrentMems user (Group gInfo members) memsToChange = case L.nonEmpty memsToChange of + changeRoleCurrentMems :: User -> Group -> Maybe VersionRoster -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) + changeRoleCurrentMems user (Group gInfo members) rosterVer memsToChange = case L.nonEmpty memsToChange of Nothing -> pure ([], [], [], False) Just memsToChange' -> do - let events = L.map (\GroupMember {memberId} -> XGrpMemRole memberId newRole) memsToChange' + let mKey m = if isJust rosterVer then MemberKey <$> memberPubKey m else Nothing + events = L.map (\m@GroupMember {memberId} -> XGrpMemRole memberId newRole (mKey m) rosterVer) memsToChange' recipients = filter memberCurrent members (msgs_, _gsr) <- sendGroupMessages user gInfo Nothing False recipients events let signed = any (either (const False) (isJust . signedMsg_)) msgs_ itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_) cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) False itemsData Nothing False when (length cis_ /= length memsToChange) $ logError "changeRoleCurrentMems: memsToChange and cis_ length mismatch" - (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_ + (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) pure (errs, changed, acis, signed) where sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c @@ -2874,20 +2888,25 @@ processChatCommand cxt nm = \case withGroupLock "removeMembers" groupId $ do -- TODO [relays] possible optimization is to read only required members + relays Group gInfo members <- withFastStore $ \db -> getGroup db cxt user groupId - let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers gmIds members + let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin, anyPrivilegedRemoved) = selectMembers gmIds members gmIds = S.fromList $ L.toList groupMemberIds memCount = length groupMemberIds when (count /= memCount) $ throwChatError CEGroupMemberNotFound when (memCount > 1 && anyAdmin) $ throwCmdError "can't remove multiple members when admins selected" assertUserGroupRole gInfo $ max GRAdmin maxRole + when (useRelays' gInfo && anyPrivilegedRemoved && memberRole' (membership gInfo) /= GROwner) $ + throwCmdError "only the group owner can remove members, moderators and admins" (errs1, deleted1) <- deleteInvitedMems user invitedMems let recipients = filter memberCurrent members - (errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing recipients currentMems + let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyPrivilegedRemoved + rosterVer <- if doBumpRoster then Just <$> reserveRosterVersion gInfo else pure Nothing + (errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing rosterVer recipients currentMems (errs3, deleted3, acis3, signed3) <- foldM (\acc m -> deletePendingMember acc user gInfo [m] m) ([], [], [], False) pendingApprvMems let moderators = filter (\GroupMember {memberRole} -> memberRole >= GRModerator) members (errs4, deleted4, acis4, signed4) <- foldM (\acc m -> deletePendingMember acc user gInfo (m : moderators) m) ([], [], [], False) pendingRvwMems + forM_ rosterVer $ \v -> broadcastRoster user gInfo v `catchAllErrors` eToView let acis = acis2 <> acis3 <> acis4 errs = errs1 <> errs2 <> errs3 <> errs4 deleted = deleted1 <> deleted2 <> deleted3 <> deleted4 @@ -2902,19 +2921,20 @@ processChatCommand cxt nm = \case unless (null errs) $ toView $ CEvtChatErrors errs pure $ CRUserDeletedMembers user gInfo' deleted withMessages msgSigned -- same order is not guaranteed where - selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) - selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False) + selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) + selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False, False) where - addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole} + addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin, anyPrivRemoved) m@GroupMember {groupMemberId, memberStatus, memberRole} | groupMemberId `S.member` gmIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin + anyPrivRemoved' = anyPrivRemoved || isRosterRole memberRole n' = n + 1 in case memberStatus of - GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin') - GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin') - GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin') - _ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin') + GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved') + GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved') + GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved') + _ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin', anyPrivRemoved') | otherwise = acc deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) deleteInvitedMems user memsToDelete = do @@ -2927,14 +2947,14 @@ processChatCommand cxt nm = \case deletePendingMember :: ([ChatError], [GroupMember], [AChatItem], Bool) -> User -> GroupInfo -> [GroupMember] -> GroupMember -> CM ([ChatError], [GroupMember], [AChatItem], Bool) deletePendingMember (accErrs, accDeleted, accACIs, accSigned) user gInfo recipients m = do (m', scopeInfo) <- mkMemberSupportChatInfo m - (errs, deleted, acis, signed) <- deleteMemsSend user gInfo (Just scopeInfo) recipients [m'] + (errs, deleted, acis, signed) <- deleteMemsSend user gInfo (Just scopeInfo) Nothing recipients [m'] pure (errs <> accErrs, deleted <> accDeleted, acis <> accACIs, accSigned || signed) - deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) - deleteMemsSend user gInfo chatScopeInfo recipients memsToDelete = case L.nonEmpty memsToDelete of + deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe VersionRoster -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) + deleteMemsSend user gInfo chatScopeInfo rosterVer recipients memsToDelete = case L.nonEmpty memsToDelete of Nothing -> pure ([], [], [], False) Just memsToDelete' -> do let chatScope = toChatScope <$> chatScopeInfo - events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages) memsToDelete' + events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages rosterVer) memsToDelete' (msgs_, _gsr) <- sendGroupMessages user gInfo chatScope False recipients events let signed = any (either (const False) (isJust . signedMsg_)) msgs_ itemsData_ = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_) @@ -3134,7 +3154,7 @@ processChatCommand cxt nm = \case (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode - void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) -- TODO not sure it is correct to set connections status here? pure $ CRNewMemberContact user ct g m _ -> throwChatError CEGroupMemberNotActive @@ -3613,13 +3633,18 @@ processChatCommand cxt nm = \case where cReqHash1 = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} cReqHash2 = contactCReqHash $ CRContactUri crData {crScheme = simplexChat} + -- relay-group joins (only via connectToRelay) carry the target relay member in preparedEntity_; + -- its memberId binds the join signature so a sibling relay can't replay it + relayMemberId_ = case preparedEntity_ of + Just (PCEGroup gInfo m) | useRelays' gInfo -> Just (memberId' m) + _ -> Nothing joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} gInfo_ = do when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection" -- TODO [relays] member: refactor joinContact and up avoiding parallel ifs, xContactId is not used xContactId <- mkXContactId xContactId_ localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId let incognitoProfile = fromLocalProfile <$> localIncognitoProfile - conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ PQSupportOn + conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ PQSupportOn pure $ CVRSentInvitation conn' incognitoProfile connect' groupLinkId xContactId_ gInfo_ = do let inGroup = isJust groupLinkId @@ -3634,7 +3659,7 @@ processChatCommand cxt nm = \case subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReq cReqHash1 sLnk' xContactId incognitoProfile_ groupLinkId subMode chatV pqSup - conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup + conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup pure $ CVRSentInvitation conn' incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse connectContactViaAddress user@User {userId} incognito ct@Contact {contactId, activeConn} (CCLink cReq shortLink) = @@ -3649,7 +3674,7 @@ processChatCommand cxt nm = \case subMode <- chatReadVar subscriptionMode let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup - void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing pqSup + void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing Nothing pqSup ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile Just conn@Connection {connStatus, xContactId = xContactId_, customUserProfileId} -> case connStatus of @@ -3658,7 +3683,7 @@ processChatCommand cxt nm = \case xContactId <- mkXContactId xContactId_ localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId let incognitoProfile = fromLocalProfile <$> localIncognitoProfile - void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing PQSupportOn + void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing Nothing PQSupportOn ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile _ -> throwCmdError "contact already has connection" @@ -3670,13 +3695,14 @@ processChatCommand cxt nm = \case r <- tryAllErrors $ do (fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink relayLinkData_ <- liftIO $ decodeLinkUserData cData - case (relayLinkData_, linkEntityId) of - (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> + relayMemberId <- case (relayLinkData_, linkEntityId) of + (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> do withFastStore $ \db -> updateRelayMemberData db cxt user relayMember (MemberId entityId) (MemberKey relayKey) p + pure $ MemberId entityId _ -> throwChatError $ CEException "relay link: no relay link data or entity id" let cReq = linkConnReq fd relayLinkToConnect = CCLink cReq (Just relayLink) - void $ connectViaContact user (Just $ PCEGroup gInfo relayMember) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing + void $ connectViaContact user (Just $ PCEGroup gInfo (relayMember {memberId = relayMemberId})) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing relayMember' <- withFastStore $ \db -> getGroupMember db cxt user (groupId' gInfo) (groupMemberId' relayMember) pure (relayLink, relayMember', r) syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM () @@ -3712,8 +3738,8 @@ processChatCommand cxt nm = \case pure (connId, chatV) mkXContactId :: Maybe XContactId -> CM XContactId mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure - joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> PQSupport -> CM Connection - joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup = do + joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> Maybe MemberId -> PQSupport -> CM Connection + joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do -- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile profileToSend <- presentUserBadge user incognitoProfile $ case gInfo_ of @@ -3721,15 +3747,11 @@ processChatCommand cxt nm = \case let allowSimplexLinks = maybe True groupUserAllowSimplexLinks gInfo_' in userProfileInGroup' user allowSimplexLinks incognitoProfile Nothing -> userProfileDirect user incognitoProfile Nothing True - chatEvent <- case gInfo_ of - Just (Just gInfo) | useRelays' gInfo -> do - let GroupInfo {membership = GroupMember {memberId}} = gInfo - memberPubKey <- case groupKeys gInfo of - Just GroupKeys {memberPrivKey} -> pure $ C.publicKey memberPrivKey - Nothing -> throwChatError $ CEInternalError "no group keys for channel membership" - pure $ XMember profileToSend memberId (MemberKey memberPubKey) - _ -> pure $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ - dm <- encodeConnInfoPQ pqSup chatV chatEvent + dm <- case gInfo_ of + Just (Just gInfo) | useRelays' gInfo -> case relayMemberId_ of + Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend + Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId" + _ -> encodeConnInfoPQ pqSup chatV $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ subMode <- chatReadVar subscriptionMode void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared ConnJoined @@ -4914,7 +4936,7 @@ runRelayGroupLinkChecks user = do then do -- TODO [relays] emit event to UI when relay own status promoted to RSActive -- CEvtGroupRelayUpdated requires GroupRelay (owner-side), not available on relay side - void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSAccepted RSActive + void $ withStore' $ \db -> updateRelayOwnStatus_ db gInfo RSActive else void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSActive RSInactive _ -> pure () _ -> pure () diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index c67ad0d514..41f70afda4 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -59,7 +59,7 @@ import Simplex.Chat.Controller import Simplex.Chat.Files import Simplex.Chat.Markdown import Simplex.Chat.Messages -import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages, encodeBinaryBatch, encodeFwdElement) +import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages, encodeBatchElement, encodeBinaryBatch, encodeFwdElement) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Operators @@ -80,6 +80,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Util (encryptFile, shuffle) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import qualified Simplex.FileTransfer.Description as FD +import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.Messaging.Agent @@ -935,9 +936,9 @@ acceptContactRequestAsync liftIO $ setCommandConnId db user cmdId connId getContact db cxt user contactId -acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> CM GroupMember +acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> Maybe GroupMember -> CM GroupMember acceptGroupJoinRequestAsync - user + user@User {userId} uclId gInfo@GroupInfo {groupProfile, membership, businessChat} cReqInvId @@ -949,12 +950,22 @@ acceptGroupJoinRequestAsync gAccepted gLinkMemRole incognitoProfile - memberKey_ = do + memberKey_ + existingMem_ = do gVar <- asks random let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted + -- a roster-established privileged member attaches a connection to its existing record (keeping + -- owner-authoritative role + key); everyone else is created fresh with the group-link role cxt <- chatStoreCxt - (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db cxt gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_ + (groupMemberId, memberId) <- case existingMem_ of + Just m -> do + -- refresh the hash placeholder name from the authenticated join profile; role + key stay roster-authoritative + withStore $ \db -> do + liftIO $ updateGroupMemberStatus db userId m initialStatus + void $ updateMemberProfile db cxt user m cReqProfile + pure (groupMemberId' m, memberId' m) + Nothing -> withStore $ \db -> + createJoiningMember db cxt gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_ let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo let Profile {displayName} = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile) GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -1169,21 +1180,47 @@ memberIntroEvt gInfo reMember = mRestrictions = memberRestrictions reMember in XGrpMemIntro mInfo mRestrictions +-- Forward the saved owner-signed roster verbatim (reusing its signed shared_msg_id), then the +-- blob chunks, so the recipient verifies the owner signature. +serveRoster :: User -> GroupInfo -> GroupMember -> CM () +serveRoster user gInfo member = + when (member `supportsVersion` groupRosterVersion) $ do + cxt <- chatStoreCxt + withStore' (\db -> getGroupRoster db gInfo) >>= \case + Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_) -> + case J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json) of + Left e -> logError $ "serveRoster: cannot decode saved roster message: " <> tshow e + Right chatMsg@ChatMessage {msgId} -> + withStore' (\db -> runExceptT $ getGroupMemberById db cxt user ownerGMId) >>= \case + Right owner -> do + let fwd = GrpMsgForward {fwdSender = FwdMember (memberId' owner) (memberShortenedName owner), fwdBrokerTs = brokerTs} + sendFwdMemberMessage member fwd (VMSigned MSSVerified sm chatMsg) + forM_ ((,) <$> msgId <*> blob_) $ \(sid, blob) -> + sendInlineBlobChunks user gInfo [member] sid blob + Left e -> logError $ "serveRoster: roster owner not found: " <> tshow e + Nothing -> pure () + -- Used in groups with relays to introduce moderators and above to a new member, -- and to announce the new member to moderators and above. -- This doesn't create introduction records in db, compared to above methods. introduceInChannel :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () introduceInChannel _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do - modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo + (owners, adminsMods) <- withStore' $ \db -> + (,) <$> getGroupOwners db cxt user gInfo <*> getGroupAdminsMods db cxt user gInfo + let modMs = owners <> adminsMods void $ sendGroupMessage' user gInfo modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing withStore' $ \db -> setMemberVectorNewRelations db subscriber [(indexInGroup m, (IDSubjectIntroduced, MRIntroduced)) | m <- modMs] - let introEvts = map (memberIntroEvt gInfo) modMs - forM_ (L.nonEmpty introEvts) $ \introEvts' -> - sendGroupMemberMessages user gInfo conn introEvts' + -- owner intros first so the joiner has the owner profile loaded before applying the saved roster (signed by the owner) + sendIntros owners + serveRoster user gInfo subscriber + sendIntros adminsMods withStore' $ \db -> setMembersVectorsNewRelation db modMs subscriberIdx IDSubjectIntroduced MRIntroduced + where + sendIntros ms = forM_ (L.nonEmpty $ map (memberIntroEvt gInfo) ms) $ \evts -> + sendGroupMemberMessages user gInfo conn evts userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks @@ -1215,6 +1252,29 @@ redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDes | hasObfuscatedSimplexLink s = Nothing | otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s +-- Roles carried by the roster; owners are on the link, not the roster. +isRosterRole :: GroupMemberRole -> Bool +isRosterRole r = r == GRMember || r == GRModerator || r == GRAdmin + +-- Drop non-privileged-role entries and de-duplicate by memberId, keeping the first. +-- Runs on the parsed roster blob. +validateGroupRoster :: [RosterMember] -> [RosterMember] +validateGroupRoster entries = + dedup S.empty $ filter (\RosterMember {role} -> isRosterRole role) entries + where + dedup _ [] = [] + dedup seen (rm@RosterMember {memberId} : rms) + | memberId `S.member` seen = dedup seen rms + | otherwise = rm : dedup (S.insert memberId seen) rms + +-- Privileged members without a known key are skipped (recipients can't verify them). +buildGroupRoster :: [GroupMember] -> [RosterMember] +buildGroupRoster mods = take maxGroupRosterSize $ mapMaybe rosterMember mods + where + rosterMember GroupMember {memberId, memberPubKey, memberRole} + | isRosterRole memberRole = (\k -> RosterMember {memberId, key = MemberKey k, role = memberRole, privileges = 0}) <$> memberPubKey + | otherwise = Nothing + sendHistory :: User -> GroupInfo -> GroupMember -> CM () sendHistory _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just conn} = @@ -1341,7 +1401,7 @@ setGroupLinkData :: NetworkRequestMode -> User -> GroupInfo -> GroupLink -> CM G setGroupLinkData nm user gInfo gLink = do cxt <- chatStoreCxt (conn, groupRelays) <- withFastStore $ \db -> - (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) + (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays linkType = if useRelays' gInfo then CCTChannel else CCTGroup sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) @@ -1351,7 +1411,7 @@ setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () setGroupLinkDataAsync user gInfo gLink = do cxt <- chatStoreCxt (conn, groupRelays) <- withStore $ \db -> - (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) + (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays setAgentConnShortLinkAsync user conn userLinkData (Just crClientData) @@ -1628,13 +1688,16 @@ sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg = chSize = fromIntegral chunkSize parseChatMessage :: Connection -> ByteString -> CM (ChatMessage 'Json) -parseChatMessage conn s = do +parseChatMessage conn s = snd <$> parseChatMessage' conn s +{-# INLINE parseChatMessage #-} + +parseChatMessage' :: Connection -> ByteString -> CM (Maybe SignedMsg, ChatMessage 'Json) +parseChatMessage' conn s = case parseChatMessages s of - [msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ _ m)) -> checkEncoding m) =<< msg + [msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ sm m)) -> (sm,) <$> checkEncoding m) =<< msg _ -> throwChatError $ CEException "parseChatMessage: single message is expected" where errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s) -{-# INLINE parseChatMessage #-} getChatScopeInfo :: StoreCxt -> User -> GroupChatScope -> CM GroupChatScopeInfo getChatScopeInfo cxt user = \case @@ -1831,6 +1894,51 @@ closeFileHandle fileId files = do h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m) liftIO $ mapM_ hClose h_ `catchAll_` pure () +-- The roster file has no chat item, so chat-item file enumeration misses it; clean it up by group. +cleanupGroupRosterFile :: User -> GroupInfo -> CM () +cleanupGroupRosterFile User {userId} GroupInfo {groupId} = do + infos <- withStore' $ \db -> getGroupRosterFileInfo db userId groupId + forM_ infos $ \(fileId, filePath_) -> do + lift $ closeFileHandle fileId rcvFiles + forM_ filePath_ removeFsFile + withStore' $ \db -> do + deleteGroupRosterFile db userId groupId + deleteGroupRosterTransfers db groupId + +-- Supersede/cancel one source relay's in-flight roster transfer: remove its on-disk file + cached +-- handle first (the cascade only does rows), then the files + transfer rows. +cleanupRosterTransfer :: GroupInfo -> GroupMemberId -> CM () +cleanupRosterTransfer gInfo fromMemberId = + withStore' (\db -> getRosterTransferId db gInfo fromMemberId) >>= mapM_ cleanupRosterTransferById + +cleanupRosterTransferById :: Int64 -> CM () +cleanupRosterTransferById transferId = do + file_ <- withStore' $ \db -> getRosterTransferFile db transferId + forM_ file_ $ \(fileId, filePath_) -> do + lift $ closeFileHandle fileId rcvFiles + forM_ filePath_ removeFsFile + withStore' $ \db -> do + deleteRosterTransferFile db transferId + deleteRosterTransfer db transferId + +-- MUST evict the cached AppendMode handle before deleting chunks, else re-driven bytes append +-- after the stale prefix and corrupt the blob. +resetRosterPartialChunks :: RcvFileTransfer -> CM () +resetRosterPartialChunks ft@RcvFileTransfer {fileId, fileStatus} = do + lift $ closeFileHandle fileId rcvFiles + forM_ (rcvFilePath fileStatus) removeFsFile + withStore' $ \db -> deleteRcvFileChunks db ft + where + rcvFilePath = \case + RFSAccepted p -> Just p + RFSConnected p -> Just p + _ -> Nothing + +removeFsFile :: FilePath -> CM () +removeFsFile fp = do + p <- lift $ toFSFilePath fp + removeFile p `catchAllErrors` \_ -> pure () + deleteMembersConnections :: User -> [GroupMember] -> CM () deleteMembersConnections user members = deleteMembersConnections' user members False @@ -2063,6 +2171,26 @@ encodeConnInfoPQ pqSup v chatMsgEvent = do _ -> pure connInfo ECMLarge -> throwChatError $ CEException "large info" +-- conn-info wrapped as a signed element, so the receiver can verify the signature over the body +encodeSignedConnInfo :: MsgEncodingI e => MsgSigning -> ChatMsgEvent e -> CM ByteString +encodeSignedConnInfo signing chatMsgEvent = do + vr <- chatVersionRange + let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent} + case encodeChatMessage maxEncodedInfoLength info of + ECMEncoded body -> pure $ encodeBatchElement (Just $ signChatMsgBody signing body) body + ECMLarge -> throwChatError $ CEException "large signed info" + +-- signed XMember for a relay-group join: proves the joiner holds the member key it asserts, and carries +-- viaRelay = the target relay's memberId inside the signed body so a sibling relay can't accept a replay +encodeXMemberConnInfo :: GroupInfo -> MemberId -> Profile -> CM ByteString +encodeXMemberConnInfo GroupInfo {membership = GroupMember {memberId}, groupKeys} relayMemberId profileToSend = + case groupKeys of + Just GroupKeys {publicGroupId, memberPrivKey} -> + let xMemberEvt = XMember profileToSend memberId (MemberKey $ C.publicKey memberPrivKey) (Just relayMemberId) + signing = MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey + in encodeSignedConnInfo signing xMemberEvt + Nothing -> throwChatError $ CEInternalError "no group keys for channel membership" + deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption) deliverMessage conn cmEventTag msgBody msgId = do let msgFlags = MsgFlags {notification = hasNotification cmEventTag} @@ -2136,6 +2264,52 @@ sendGroupMessage' user gInfo members chatMsgEvent = ((Right msg) :| [], _) -> pure msg _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" +-- TODO [relays] improvement: publish roster_version in link data so the owner can recover the latest version +-- TODO after restoring from a stale backup (relays accept only strictly-greater versions) +-- Persist the next roster version before sending the events that carry it (so a recipient never advances +-- past a version the owner hasn't recorded). The matching blob is broadcast separately, by broadcastRoster, +-- after the change is applied to the owner's members - so the served roster excludes demoted/removed members. +reserveRosterVersion :: GroupInfo -> CM VersionRoster +reserveRosterVersion gInfo = do + let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo) + withStore' $ \db -> setGroupRosterVersion db gInfo rosterVer + pure rosterVer + +broadcastRoster :: User -> GroupInfo -> VersionRoster -> CM () +broadcastRoster user gInfo rosterVer = do + cxt <- chatStoreCxt + (relays, rosterMems) <- withStore' $ \db -> + (,) <$> getGroupRelayMembers db cxt user gInfo <*> getGroupRosterMembers db cxt user gInfo + forM_ (L.nonEmpty relays) $ \relays' -> + sendRoster user gInfo (L.toList relays') rosterVer (buildGroupRoster rosterMems) + +-- Send the current roster (no version bump) to a newly added relay so it can serve joiners. +sendGroupRosterToRelay :: User -> GroupInfo -> GroupMember -> CM () +sendGroupRosterToRelay user gInfo relayMember = + forM_ (rosterVersion gInfo) $ \rosterVer -> do + cxt <- chatStoreCxt + rosterMems <- withStore' $ \db -> getGroupRosterMembers db cxt user gInfo + sendRoster user gInfo [relayMember] rosterVer (buildGroupRoster rosterMems) + +-- Row-less send (no files/snd_files rows, so no send-side cleanup); redelivery is the agent's. +sendRoster :: User -> GroupInfo -> [GroupMember] -> VersionRoster -> [RosterMember] -> CM () +sendRoster user gInfo members rosterVer roster = do + let blob = encodeRosterBlob roster + fileInv = InlineFileInvitation {fileSize = fromIntegral (B.length blob), fileDigest = FD.FileDigest $ LC.sha512Hash $ LB.fromStrict blob} + SndMessage {sharedMsgId} <- sendGroupMessage' user gInfo members (XGrpRoster GroupRoster {version = rosterVer, fileInv}) + sendInlineBlobChunks user gInfo members sharedMsgId blob + +-- Send a binary blob as BFileChunks under a shared_msg_id to the given members (chunked by fileChunkSize). +sendInlineBlobChunks :: User -> GroupInfo -> [GroupMember] -> SharedMsgId -> ByteString -> CM () +sendInlineBlobChunks user gInfo members sharedMsgId blob = do + chSize <- fromIntegral <$> asks (fileChunkSize . config) + go chSize 1 blob + where + go chSize chunkNo bytes = do + let (chunk, rest) = B.splitAt chSize bytes + void $ sendGroupMessage' user gInfo members (BFileChunk sharedMsgId (FileChunk chunkNo chunk)) + unless (B.null rest) $ go chSize (chunkNo + 1) rest + -- Relay advertises its current web preview capability to channel owners. -- Idempotent: sends only when the configured web domain differs from what was last sent, and only to -- owners whose recorded chat version supports relayWebCapVersion (older apps can't parse XGrpRelayCap). @@ -2372,10 +2546,14 @@ saveDirectRcvMSG conn@Connection {connId} agentMsgMeta chatMsg@ChatMessage {chat msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing pure (conn', msg) -saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> VerifiedMsg e -> CM (GroupMember, Connection, RcvMessage) +saveGroupRcvMsg :: forall e. MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> VerifiedMsg e -> CM (GroupMember, Connection, RcvMessage) saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta verifiedMsg = do let ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = verifiedChatMsg verifiedMsg - (am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- updateMemberChatVRange authorMember conn chatVRange + -- binary messages (file chunks) carry only the initial-version sentinel, not the sender's range; + -- applying it would downgrade the member's negotiated version and suppress version-gated delivery + (am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- case encoding @e of + SBinary -> pure (authorMember, conn) + SJson -> updateMemberChatVRange authorMember conn chatVRange let agentMsgId = fst $ recipient agentMsgMeta brokerTs = metaBrokerTs agentMsgMeta newMsg = NewRcvMessage {chatMsgEvent, verifiedMsg, brokerTs} @@ -2513,11 +2691,11 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, msgSigned, forwardedByMem _ -> Nothing -- TODO [mentions] optimize by avoiding unnecessary parsing -mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d -mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs = +mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> Maybe MsgSigStatus -> UTCTime -> ChatItem c d +mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember msgSigned currentTs = let ts@(_, ft_) = ciContentTexts content hasLink_ = ciContentHasLink content ft_ - in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember Nothing currentTs + in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember msgSigned currentTs mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> Maybe MsgSigStatus -> UTCTime -> ChatItem c d mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember msgSigned currentTs = @@ -2679,7 +2857,7 @@ createFeatureEnabledItems_ :: User -> Contact -> CM [AChatItem] createFeatureEnabledItems_ user ct@Contact {mergedPreferences} = forM allChatFeatures $ \(ACF f) -> do let state = featureState $ getContactUserPreference f mergedPreferences - createChatItem user (CDDirectRcv ct) False (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing Nothing + createChatItem user (CDDirectRcv ct) False (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing Nothing Nothing createFeatureItems :: MsgDirectionI d => @@ -2709,15 +2887,15 @@ createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do unless (null errs) $ toView' $ CEvtChatErrors errs toView' $ CEvtNewChatItems user acis where - contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) + contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures (chatDir ct', False, contents) where - featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d, Maybe SharedMsgId) + featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus) featureCIContent_ f - | state /= state' = Just (fContent ciFeature state', Nothing) - | prefState /= prefState' = Just (fContent ciOffer prefState', Nothing) + | state /= state' = Just (fContent ciFeature state', Nothing, Nothing) + | prefState /= prefState' = Just (fContent ciOffer prefState', Nothing, Nothing) | otherwise = Nothing where fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d @@ -2750,16 +2928,16 @@ createGroupFeatureItems_ user cd showGroupAsSender ciContent GroupInfo {fullGrou forM allGroupFeatures $ \(AGF f) -> do let p = getGroupPreference f fullGroupPreferences (_, param, role) = groupFeatureState p - createChatItem user cd showGroupAsSender (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing Nothing + createChatItem user cd showGroupAsSender (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing Nothing Nothing createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM () createInternalChatItem user cd content itemTs_ = do - ci <- createChatItem user cd False content Nothing itemTs_ + ci <- createChatItem user cd False content Nothing Nothing itemTs_ toView $ CEvtNewChatItems user [ci] -createChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Maybe UTCTime -> CM AChatItem -createChatItem user cd showGroupAsSender content sharedMsgId itemTs_ = - lift (createChatItems user itemTs_ [(cd, showGroupAsSender, [(content, sharedMsgId)])]) >>= \case +createChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Maybe MsgSigStatus -> Maybe UTCTime -> CM AChatItem +createChatItem user cd showGroupAsSender content sharedMsgId msgSigned itemTs_ = + lift (createChatItems user itemTs_ [(cd, showGroupAsSender, [(content, sharedMsgId, msgSigned)])]) >>= \case [Right ci] -> pure ci [Left e] -> throwError e rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs) @@ -2771,7 +2949,7 @@ createChatItems :: (ChatTypeI c, MsgDirectionI d) => User -> Maybe UTCTime -> - [(ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)])] -> + [(ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)])] -> CM' [Either ChatError AChatItem] createChatItems user itemTs_ dirsCIContents = do createdAt <- liftIO getCurrentTime @@ -2780,24 +2958,24 @@ createChatItems user itemTs_ dirsCIContents = do void . withStoreBatch' $ \db -> map (updateChat db cxt createdAt) dirsCIContents withStoreBatch' $ \db -> concatMap (createACIs db itemTs createdAt) dirsCIContents where - updateChat :: DB.Connection -> StoreCxt -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> IO () + updateChat :: DB.Connection -> StoreCxt -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) -> IO () updateChat db cxt createdAt (cd, _, contents) - | any (ciRequiresAttention . fst) contents || contactChatDeleted cd = void $ updateChatTsStats db cxt user cd createdAt memberChatStats + | any (\(content, _, _) -> ciRequiresAttention content) contents || contactChatDeleted cd = void $ updateChatTsStats db cxt user cd createdAt memberChatStats | otherwise = pure () where memberChatStats :: Maybe (Int, MemberAttention, Int) memberChatStats = case cd of CDGroupRcv _g (Just scope) m -> do - let unread = length $ filter (ciRequiresAttention . fst) contents + let unread = length $ filter (\(content, _, _) -> ciRequiresAttention content) contents in Just (unread, memberAttentionChange unread itemTs_ (Just m) scope, 0) _ -> Nothing - createACIs :: DB.Connection -> UTCTime -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> [IO AChatItem] + createACIs :: DB.Connection -> UTCTime -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) -> [IO AChatItem] createACIs db itemTs createdAt (cd, showGroupAsSender, contents) = map createACI contents where - createACI (content, sharedMsgId) = do + createACI (content, sharedMsgId, msgSigned) = do let hasLink_ = ciContentHasLink content Nothing - ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ itemTs createdAt - let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt + ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ msgSigned itemTs createdAt + let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing msgSigned createdAt pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci -- rcvMem_ Nothing means message from channel - treated same as message from moderator, diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 2664e217f7..a2ea9870cc 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -24,6 +24,7 @@ import Control.Monad.IO.Unlift import Control.Monad.Reader import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB import Data.Either (lefts, partitionEithers, rights) import Data.Foldable (foldr', foldrM) import Data.Functor (($>)) @@ -40,12 +41,14 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime) +import Data.Time.Format (defaultTimeLocale, formatTime) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Data.Word (Word32) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Delivery +import Simplex.Chat.Files (getChatTempDirectory) import Simplex.Chat.Library.Internal import Simplex.Chat.Web (channelContentChanged, channelProfileUpdated, channelRemoved) import Simplex.Chat.Messages @@ -77,7 +80,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Client (getAgentWorker, temporaryOrHostError, waitForUserNetwork, waitForWork, waitWhileSuspended, withWorkItems, withWork_) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Worker (..)) +import Simplex.Messaging.Agent.Env.SQLite (Worker (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay) @@ -87,8 +90,10 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR +import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding (smpEncode) import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ErrorType (..), MsgFlags (..), ServiceSub (..), ServiceSubError (..), ServiceSubResult (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) @@ -106,6 +111,13 @@ import UnliftIO.STM smallGroupsRcptsMemLimit :: Int smallGroupsRcptsMemLimit = 20 +-- Verifies member signatures over CBGroup <> (publicGroupId, memberId) <> signedBody under the given key. +-- signatures is NonEmpty so the verification can't be vacuously true. +verifyGroupSig :: C.PublicKeyEd25519 -> B64UrlByteString -> MemberId -> NonEmpty MsgSignature -> ByteString -> Bool +verifyGroupSig key publicGroupId memberId signatures signedBody = + let prefix = smpEncode CBGroup <> smpEncode (publicGroupId, memberId) + in all (\case (MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 key) sig (prefix <> signedBody)) signatures + processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessage _ _ (DEL_RCVQS delQs) = toView $ CEvtAgentRcvQueuesDeleted $ L.map rcvQ delQs @@ -576,7 +588,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = (gInfo, host) <- withStore $ \db -> do liftIO $ deleteContactCardKeepConn db connId ct createGroupInvitedViaLink db cxt user conn'' glInv - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) @@ -901,8 +913,21 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = GCInviteeMember | isRelay m -> do withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected - gLink <- withStore $ \db -> getGroupLink db user gInfo - setGroupLinkDataAsync user gInfo gLink + if m `supportsVersion` groupRosterVersion + then do + -- send the relay a roster (materializing version 0 for old channels with NULL roster_version); + -- the relay stays RSInvited (unpublishable) until it acks, so no joiner can impersonate a privileged member + gInfo' <- case rosterVersion gInfo of + Just _ -> pure gInfo + Nothing -> do + withStore' $ \db -> setGroupRosterVersion db gInfo (VersionRoster 0) + pure gInfo {rosterVersion = Just (VersionRoster 0)} + sendGroupRosterToRelay user gInfo' m + else do + -- a relay below groupRosterVersion can't ack a roster; publish it on connect as before + -- the handshake (getPublishableGroupRelays and the LINK handler include/activate it by version) + gLink <- withStore $ \db -> getGroupLink db user gInfo + setGroupLinkDataAsync user gInfo gLink | otherwise -> do (gInfo', mStatus) <- if not (memberPending m) @@ -1024,8 +1049,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure newDeliveryTasks processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> VerifiedMsg e -> CM (Maybe NewMessageDeliveryTask) processEvent gInfo' m' verifiedMsg = do - (m'', conn', msg@RcvMessage {msgId, chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta verifiedMsg cc <- ask + (m'', conn', msg@RcvMessage {msgId, sharedMsgId_, chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta verifiedMsg let ctx js = DeliveryTaskContext js False checkSendAsGroup :: Maybe Bool -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext) checkSendAsGroup asGroup_ a @@ -1064,23 +1089,25 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv XGrpMemFwd memInfo introInv -> Nothing <$ xGrpMemFwd gInfo' m'' memInfo introInv - XGrpMemRole memId memRole -> fmap ctx <$> xGrpMemRole gInfo' m'' memId memRole msg brokerTs + XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole gInfo' m'' memId memRole memberKey rosterVer msg brokerTs XGrpMemRestrict memId memRestrictions -> fmap ctx <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId - XGrpMemDel memId withMessages -> case encoding @e of - SJson -> fmap ctx <$> xGrpMemDel gInfo' m'' memId withMessages verifiedMsg msg brokerTs False + XGrpMemDel memId withMessages rosterVer -> case encoding @e of + SJson -> fmap ctx <$> xGrpMemDel gInfo' m'' memId withMessages rosterVer verifiedMsg msg brokerTs False SBinary -> pure Nothing XGrpLeave -> fmap ctx <$> xGrpLeave gInfo' m'' msg brokerTs XGrpDel -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' msg brokerTs XGrpInfo p' -> fmap ctx <$> xGrpInfo gInfo' m'' p' msg brokerTs XGrpPrefs ps' -> fmap ctx <$> xGrpPrefs gInfo' m'' ps' msg + XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' m'' gr verifiedMsg sharedMsgId_ brokerTs + XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck gInfo' m'' ackVer ackErr -- TODO [knocking] why don't we forward these messages? XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward gInfo' Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs XInfoProbe probe -> Nothing <$ xInfoProbe (COMGroupMember m'') probe XInfoProbeCheck probeHash -> Nothing <$ xInfoProbeCheck (COMGroupMember m'') probeHash XInfoProbeOk probe -> Nothing <$ xInfoProbeOk (COMGroupMember m'') probe - BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' sharedMsgId chunk msgMeta + BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' m'' sharedMsgId chunk msgMeta _ -> Nothing <$ messageError ("unsupported message: " <> tshow event) forM deliveryTaskContext_ $ \taskContext -> do let contentChanged :: CM () @@ -1143,7 +1170,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId updateGroupItemsStatus gInfo m conn msgId GSSSent (Just $ isJust proxy) - when continued $ sendPendingGroupMessages user gInfo m conn + when continued $ do + when (isUserGrpFwdRelay gInfo) $ serveRoster user gInfo m -- roster ahead of the resumed backlog + sendPendingGroupMessages user gInfo m conn SWITCH qd phase cStats -> do toView $ CEvtGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m @@ -1195,9 +1224,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = CFGetRelayDataJoin -> do -- Update relay member with key, memberId and profile from link relayLinkData_ <- liftIO $ decodeLinkUserData cData - case (relayLinkData_, linkEntityId) of - (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> + relayMemberId <- case (relayLinkData_, linkEntityId) of + (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> do withStore $ \db -> updateRelayMemberData db cxt user m (MemberId entityId) (MemberKey relayKey) p + pure $ MemberId entityId _ -> throwChatError $ CEException "relay link: no relay link data or entity id" case cReq of CRContactUri crData@ConnReqUriData {crClientData} -> do @@ -1210,13 +1240,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} -- Update connection with data derived from cReq, now available after getConnShortLinkAsync withStore' $ \db -> updateConnLinkData db user conn cReq cReqHash groupLinkId chatV pqSup - let GroupMember {memberId = membershipMemId} = membership - incognitoProfile = incognitoMembershipProfile gInfo - profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) - memberPubKey <- case groupKeys gInfo of - Just GroupKeys {memberPrivKey} -> pure $ C.publicKey memberPrivKey - Nothing -> throwChatError $ CEInternalError "no group keys for channel membership" - dm <- encodeConnInfo $ XMember profileToSend membershipMemId (MemberKey memberPubKey) + let incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo + profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo incognitoProfile + dm <- encodeXMemberConnInfo gInfo relayMemberId profileToSend subMode <- chatReadVar subscriptionMode void $ joinAgentConnectionAsync user (Just conn) True cReq dm subMode CFGetRelayDataAccept -> do @@ -1226,7 +1252,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = relayProfile <- liftIO (decodeLinkUserData cData) >>= \case Just RelayShortLinkData {relayProfile = p} -> pure p Nothing -> throwChatError $ CEException "relay link: no relay link data" - (confId, m', relay) <- withStore $ \db -> do + (confId, m', relay) <- withStore $ \db -> do confId <- getRelayConfId db m liftIO $ updateGroupMemberStatus db userId m GSMemAccepted (m', relay) <- setRelayLinkAccepted db cxt user m (MemberKey relayKey) relayProfile @@ -1239,7 +1265,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = _ -> throwChatError $ CECommandError "unexpected cmdFunction" QCONT -> do continued <- continueSending connEntity conn - when continued $ sendPendingGroupMessages user gInfo m conn + when continued $ do + when (isUserGrpFwdRelay gInfo) $ serveRoster user gInfo m -- roster ahead of the resumed backlog + sendPendingGroupMessages user gInfo m conn MWARN msgId err -> do withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSWarning $ agentSndError err) processConnMWARN connEntity conn err @@ -1312,13 +1340,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = r n'' = Just (ci, CIRcvDecryptionError mde n'') mdeUpdatedCI _ _ = Nothing - receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> CM () - receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case - FileChunkCancel -> - unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft - ci <- withStore $ \db -> getChatItemByFileId db cxt user fileId - toView $ CEvtRcvFileSndCancelled user ci ft + receiveFileChunk :: Maybe GroupInfo -> RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> CM () + receiveFileChunk gInfo_ ft@RcvFileTransfer {fileId, fileType, chunkSize} conn_ MsgMeta {recipient = (msgId, _), integrity} = \case + FileChunkCancel -> case fileType of + -- cancel only this source's transfer; other relays' in-flight transfers are independent + FTRoster -> do + t_ <- withStore' $ \db -> getRosterTransfer db fileId + forM_ t_ $ \RcvRosterTransfer {rosterTransferId} -> cleanupRosterTransferById rosterTransferId + FTNormal -> + unless (rcvFileCompleteOrCancelled ft) $ do + cancelRcvFileTransfer user ft + ci <- withStore $ \db -> getChatItemByFileId db cxt user fileId + toView $ CEvtRcvFileSndCancelled user ci ft FileChunk {chunkNo, chunkBytes = chunk} -> do case integrity of MsgOk -> pure () @@ -1329,30 +1362,33 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = RcvChunkOk -> if B.length chunk /= fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" - else withAckMessage' "file msg" agentConnId meta $ appendFileChunk ft chunkNo chunk False + else appendFileChunk ft chunkNo chunk False RcvChunkFinal -> if B.length chunk > fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" else do appendFileChunk ft chunkNo chunk True - ci <- withStore $ \db -> do - liftIO $ do - updateRcvFileStatus db fileId FSComplete - updateCIFileStatus db user fileId CIFSRcvComplete - deleteRcvFileChunks db ft - getChatItemByFileId db cxt user fileId - toView $ CEvtRcvFileComplete user ci - mapM_ (deleteAgentConnectionAsync . aConnId) conn_ - RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () + case fileType of + FTRoster -> forM_ gInfo_ $ \gInfo -> rosterCompletion gInfo ft + FTNormal -> do + ci <- withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db fileId FSComplete + updateCIFileStatus db user fileId CIFSRcvComplete + deleteRcvFileChunks db ft + getChatItemByFileId db cxt user fileId + toView $ CEvtRcvFileComplete user ci + mapM_ (deleteAgentConnectionAsync . aConnId) conn_ + RcvChunkDuplicate -> pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo processContactConnMessage :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () processContactConnMessage agentMsg connEntity conn UserContact {userContactLinkId = uclId, groupId = ucGroupId_} = case agentMsg of REQ invId pqSupport _ connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + (signedMsg_, ChatMessage {chatVRange, chatMsgEvent}) <- parseChatMessage' conn connInfo case chatMsgEvent of XContact p xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport - XMember p joiningMemberId joiningMemberKey -> memberJoinRequestViaRelay invId chatVRange p joiningMemberId joiningMemberKey + XMember p joiningMemberId joiningMemberKey viaRelay -> memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey viaRelay XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport XGrpRelayInv groupRelayInv -> xGrpRelayInv invId chatVRange groupRelayInv XGrpRelayTest challenge _ -> xGrpRelayTest invId chatVRange challenge @@ -1364,13 +1400,13 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = CFSetShortLink -> case (ucGroupId_, auData) of (Just groupId, UserContactLinkData UserContactData {relays = relayLinks}) -> do - (gInfo, gLink, relays, relaysChanged, newlyActiveLinks) <- withStore $ \db -> do + (gInfo, gLink, relays, relaysChanged, newlyActiveLinks, newlyActiveGMIds) <- withStore $ \db -> do gInfo <- getGroupInfo db cxt user groupId gLink <- getGroupLink db user gInfo relays <- liftIO $ getGroupRelays db gInfo - (relays', changed, newlyActive) <- liftIO $ foldrM (updateRelay db) ([], False, []) relays + (relays', changed, newlyActiveLinks, newlyActiveGMIds) <- liftIO $ foldrM (updateRelay db) ([], False, [], []) relays liftIO $ setGroupInProgressDone db gInfo - pure (gInfo, gLink, relays', changed, newlyActive) + pure (gInfo, gLink, relays', changed, newlyActiveLinks, newlyActiveGMIds) toView $ CEvtGroupLinkDataUpdated user gInfo gLink relays relaysChanged let GroupSummary {publicMemberCount} = groupSummary gInfo -- Owner is counted in publicMemberCount; > 1 means at least one subscriber. @@ -1388,14 +1424,16 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = unless (null recipients) $ void $ sendGroupMessages user gInfo Nothing False recipients events where - updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact]) -> IO ([GroupRelay], Bool, [ShortLinkContact]) - updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed, newlyActive) = + updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId]) -> IO ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId]) + updateRelay db relay@GroupRelay {groupMemberId, relayLink, relayStatus} (acc, changed, newlyActiveLinks, newlyActiveGMIds) = case relayLink of Just rLink - | rLink `elem` relayLinks && relayStatus == RSAccepted -> do + -- version is gated upstream at publish (getPublishableGroupRelays): an RSAccepted relay + -- whose link is in the published data is necessarily pre-roster, so activate it too + | rLink `elem` relayLinks && (relayStatus == RSAcknowledgedRoster || relayStatus == RSAccepted) -> do relay' <- updateRelayStatus db relay RSActive - pure (relay' : acc, True, rLink : newlyActive) - | rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActive) + pure (relay' : acc, True, rLink : newlyActiveLinks, groupMemberId : newlyActiveGMIds) + | rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActiveLinks, newlyActiveGMIds) | relayStatus == RSActive -> do -- Relay link absent from link data — deactivate. -- RSAccepted relays are not deactivated: their own link data update @@ -1404,8 +1442,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- TODO the SMP server, but this owner won't receive a LINK callback for it -- TODO (LINK only fires in response to own setConnShortLink calls). relay' <- updateRelayStatus db relay RSInactive - pure (relay' : acc, True, newlyActive) - _ -> pure (relay : acc, changed, newlyActive) + pure (relay' : acc, True, newlyActiveLinks, newlyActiveGMIds) + _ -> pure (relay : acc, changed, newlyActiveLinks, newlyActiveGMIds) _ -> throwChatError $ CECommandError "LINK event expected for a group link only" _ -> throwChatError $ CECommandError "unexpected cmdFunction" MERR _ err -> do @@ -1446,12 +1484,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- they will be updated after connection is accepted. upsertDirectRequestItem cd (requestMsg_, prevSharedMsgId_) Nothing -> do - void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) let e2eContent = CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ Just $ CR.pqSupportToEnc $ reqPQSup - void $ createChatItem user cd False e2eContent Nothing Nothing + void $ createChatItem user cd False e2eContent Nothing Nothing Nothing void $ createFeatureEnabledItems_ user ct forM_ (autoReply addressSettings) $ \mc -> forM_ welcomeSharedMsgId $ \sharedMsgId -> - createChatItem user (CDDirectSnd ct) False (CISndMsgContent mc) (Just sharedMsgId) Nothing + createChatItem user (CDDirectSnd ct) False (CISndMsgContent mc) (Just sharedMsgId) Nothing Nothing mapM (createRequestItem cd) requestMsg_ case autoAccept of Nothing -> do @@ -1476,13 +1514,13 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- they will be updated after connection is accepted. upsertBusinessRequestItem cd (requestMsg_, prevSharedMsgId_) Nothing -> do - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) -- TODO [short links] possibly, we can just keep them created where they are created on the business side due to auto-accept -- let e2eContent = CIRcvGroupE2EEInfo $ E2EInfo $ Just False -- no PQ encryption in groups - -- void $ createChatItem user cd False e2eContent Nothing Nothing + -- void $ createChatItem user cd False e2eContent Nothing Nothing Nothing -- void $ createFeatureEnabledItems_ user ct forM_ (autoReply addressSettings) $ \arMC -> forM_ welcomeSharedMsgId $ \sharedMsgId -> - createChatItem user (CDGroupSnd gInfo Nothing) False (CISndMsgContent arMC) (Just sharedMsgId) Nothing + createChatItem user (CDGroupSnd gInfo Nothing) False (CISndMsgContent arMC) (Just sharedMsgId) Nothing Nothing mapM (createRequestItem cd) requestMsg_ toView $ CEvtAcceptingBusinessRequest user gInfo where @@ -1546,7 +1584,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = upsertBusinessRequestItem (CDChannelRcv _ _) = const $ pure Nothing createRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> (SharedMsgId, MsgContent) -> CM AChatItem createRequestItem cd (sharedMsgId, mc) = do - aci <- createChatItem user cd False (CIRcvMsgContent mc) (Just sharedMsgId) Nothing + aci <- createChatItem user cd False (CIRcvMsgContent mc) (Just sharedMsgId) Nothing Nothing toView $ CEvtNewChatItems user [aci] pure aci upsertRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> ((SharedMsgId, MsgContent) -> CM (Maybe AChatItem)) -> (SharedMsgId -> CM ()) -> (Maybe (SharedMsgId, MsgContent), Maybe SharedMsgId) -> CM (Maybe AChatItem) @@ -1574,7 +1612,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = messageError "processContactConnMessage: chat version range incompatible for accepting group join request" | otherwise -> do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing Nothing (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' @@ -1613,19 +1651,37 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = where User {userChatRelay} = user -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays - -- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember) - memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Profile -> MemberId -> MemberKey -> CM () - memberJoinRequestViaRelay invId chatVRange p joiningMemberId joiningMemberKey = do + memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Maybe SignedMsg -> Profile -> MemberId -> MemberKey -> Maybe MemberId -> CM () + memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey@(MemberKey joiningKey) viaRelay = do (_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId case gLinkInfo_ of Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted gLinkMemRole Nothing (Just joiningMemberKey) + existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db cxt user gInfo joiningMemberId) + case existing_ of + Just rosterMem + -- a privileged memberId's key is owner-authoritative (the roster); the joiner must prove + -- possession of that exact key, otherwise this is an attempt to impersonate it + | isRosterRole (memberRole' rosterMem) -> + if verifyKey gInfo rosterMem + then acceptJoin gInfo (Just rosterMem) (memberRole' rosterMem) + else messageError "memberJoinRequestViaRelay: rejected join claiming privileged memberId (key mismatch or invalid signature)" + _ -> acceptJoin gInfo Nothing gLinkMemRole + Nothing -> + messageError "memberJoinRequestViaRelay: no group link info for relay link" + where + -- replay defense: the viaRelay == own memberId check (viaRelay is in the signed body); without it a sibling relay could replay a privileged member's signed join + verifyKey gInfo rosterMem = case (signedMsg_, groupKeys gInfo) of + (Just SignedMsg {chatBinding = CBGroup, signatures, signedBody}, Just GroupKeys {publicGroupId}) -> + memberPubKey rosterMem == Just joiningKey + && verifyGroupSig joiningKey publicGroupId joiningMemberId signatures signedBody + && viaRelay == Just (memberId' (membership gInfo)) + _ -> False + acceptJoin gInfo existingMem_ acceptRole = do + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted acceptRole Nothing (Just joiningMemberKey) existingMem_ (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' - Nothing -> - messageError "memberJoinRequestViaRelay: no group link info for relay link" muteEventInChannel :: GroupInfo -> GroupMember -> Bool muteEventInChannel gInfo@GroupInfo {membership} m = @@ -2157,7 +2213,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = unless (maybe False memberBlocked m') $ autoAcceptFile file_ processFileInv gInfo' m' = let fileMember_ = if sentAsGroup then Nothing else m' - in processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ + in processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ FTNormal sharedMsgId_ newChatItem gInfo' m' scopeInfo ciContent ciFile_ timed live = do let mentions' = if maybe False memberBlocked m' then M.empty else mentions (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo ciContent ciFile_ timed live mentions' @@ -2365,7 +2421,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = ChatConfig {fileChunkSize} <- asks config fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv inline <- receiveInlineMode fInv' Nothing fileChunkSize - RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) fInv' inline fileChunkSize + RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) FTNormal sharedMsgId_ fInv' inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" @@ -2461,10 +2517,17 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = ft <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId >>= getRcvFileTransfer db user receiveInlineChunk ft chunk meta - bFileChunkGroup :: GroupInfo -> SharedMsgId -> FileChunk -> MsgMeta -> CM () - bFileChunkGroup GroupInfo {groupId} sharedMsgId chunk meta = do - ft <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId >>= getRcvFileTransfer db user - receiveInlineChunk ft chunk meta + -- A group BFileChunk is a normal inline file chunk or a roster blob chunk, both located by + -- (group_id, shared_msg_id). A chunk matching no in-flight transfer (an orphaned re-served roster + -- chunk, or a missing normal file) is ignored; the outer withAckMessage acks it. + bFileChunkGroup :: GroupInfo -> GroupMember -> SharedMsgId -> FileChunk -> MsgMeta -> CM () + bFileChunkGroup gInfo@GroupInfo {groupId} fromMember sharedMsgId chunk meta = do + fileId_ <- withStore' $ \db -> getGroupRcvFileId db userId groupId (groupMemberId' fromMember) sharedMsgId + forM_ fileId_ $ \fileId -> do + ft <- withStore $ \db -> getRcvFileTransfer db user fileId + case fileType ft of + FTRoster -> receiveRosterChunk gInfo ft meta chunk + FTNormal -> receiveInlineChunk ft chunk meta receiveInlineChunk :: RcvFileTransfer -> FileChunk -> MsgMeta -> CM () receiveInlineChunk RcvFileTransfer {fileId, fileStatus = RFSNew} FileChunk {chunkNo} _ @@ -2474,7 +2537,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = case chunk of FileChunk {chunkNo} -> when (chunkNo == 1) $ startReceivingFile user fileId _ -> pure () - receiveFileChunk ft Nothing meta chunk + receiveFileChunk Nothing ft Nothing meta chunk + + -- A roster re-serve re-sends the blob from chunk 1; discard any partial first, else chunk 1 over a + -- partial is out-of-order (RcvChunkError) and appending after the stale prefix corrupts the blob. + receiveRosterChunk :: GroupInfo -> RcvFileTransfer -> MsgMeta -> FileChunk -> CM () + receiveRosterChunk gInfo ft meta chunk = do + case chunk of + FileChunk {chunkNo} | chunkNo == 1 -> do + last_ <- withStore' $ \db -> getRcvFileLastChunkNo db ft + when (isJust last_) $ resetRosterPartialChunks ft + _ -> pure () + receiveFileChunk (Just gInfo) ft Nothing meta chunk xFileCancelGroup :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> CM (Maybe DeliveryTaskContext) xFileCancelGroup g@GroupInfo {groupId} m_ sharedMsgId = do @@ -2532,7 +2606,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let GroupMember {groupMemberId, memberId = membershipMemId} = membership if sameGroupLinkId groupLinkId groupLinkId' then do @@ -2996,40 +3070,63 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = _ -> pure (conn', Nothing) xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ _) msgScope_ msg brokerTs = do - if useRelays' gInfo && isRelay m - then when (memRole > GRMember) $ throwChatError $ CEException "x.grp.mem.new: relay cannot introduce role above member in channel" - else checkHostRole m memRole + xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ assertedKey_) msgScope_ msg brokerTs = do + let fromRelay = useRelays' gInfo && isRelay m + unless fromRelay $ checkHostRole m memRole if sameMemberId memId (membership gInfo) then pure Nothing - else do + else withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case - Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do - (updatedMember, gInfo') <- withStore $ \db -> do - updatedMember <- updateUnknownMemberAnnounced db cxt user m unknownMember memInfo initialStatus - gInfo' <- - if memberPending updatedMember - then liftIO $ increaseGroupMembersRequireAttention db user gInfo - else pure gInfo - pure (updatedMember, gInfo') - gInfo'' <- updatePublicGroupData user gInfo' - toView $ CEvtUnknownMemberAnnounced user gInfo'' m unknownMember updatedMember - memberAnnouncedToView updatedMember gInfo'' - pure $ deliveryJobScope updatedMember + Right unknownMember@GroupMember {memberStatus = GSMemUnknown} + -- roster-established privileged member: the relay may update the profile only, + -- never the role or key (those are owner-authoritative via the roster, and + -- XGrpMemNew is unsigned) + | fromRelay && isRosterRole (memberRole' unknownMember) -> do + -- a member's key is immutable per memberId and identical across relays; mismatch + -- is unambiguous relay misbehavior (role can legitimately differ across relays + -- under multi-relay skew, so we deliberately don't warn on role) + let assertedKey = (\(MemberKey k) -> k) <$> assertedKey_ + -- TODO [relays] member: surface relay-key-mismatch as a dedicated event / chat item / relay state + when (assertedKey /= memberPubKey unknownMember) $ + messageWarning $ "x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" <> safeDecodeUtf8 (strEncode memId) + updatedMember <- withStore $ \db -> updateRosterMemberAnnounced db cxt user m unknownMember memInfo initialStatus + -- roster members can't be pending, so no members-require-attention update + gInfo' <- updatePublicGroupData user gInfo + toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember + memberAnnouncedToView updatedMember gInfo' + pure $ deliveryJobScope updatedMember + -- asserted privileged but NOT roster-established: relay conjuring a moderator + | fromRelay && isRosterRole memRole -> + messageError "x.grp.mem.new: privileged role not established by roster" $> Nothing + | otherwise -> do + (updatedMember, gInfo') <- withStore $ \db -> do + updatedMember <- updateUnknownMemberAnnounced db cxt user m unknownMember memInfo initialStatus + gInfo' <- + if memberPending updatedMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo + pure (updatedMember, gInfo') + gInfo'' <- updatePublicGroupData user gInfo' + toView $ CEvtUnknownMemberAnnounced user gInfo'' m unknownMember updatedMember + memberAnnouncedToView updatedMember gInfo'' + pure $ deliveryJobScope updatedMember Right _ | useRelays' gInfo -> logInfo "x.grp.mem.new: member already created via another relay" $> Nothing | otherwise -> messageError "x.grp.mem.new error: member already exists" $> Nothing - Left _ -> do - (newMember, gInfo') <- withStore $ \db -> do - newMember <- createNewGroupMember db cxt user gInfo m memInfo GCPostMember initialStatus - gInfo' <- - if memberPending newMember - then liftIO $ increaseGroupMembersRequireAttention db user gInfo - else pure gInfo - pure (newMember, gInfo') - gInfo'' <- updatePublicGroupData user gInfo' - memberAnnouncedToView newMember gInfo'' - pure $ deliveryJobScope newMember + Left _ + -- a privileged member absent from the roster is a relay conjuring a moderator + | fromRelay && isRosterRole memRole -> messageError "x.grp.mem.new: privileged member not established by roster" $> Nothing + | otherwise -> do + (newMember, gInfo') <- withStore $ \db -> do + newMember <- createNewGroupMember db cxt user gInfo m memInfo GCPostMember initialStatus + gInfo' <- + if memberPending newMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo + pure (newMember, gInfo') + gInfo'' <- updatePublicGroupData user gInfo' + memberAnnouncedToView newMember gInfo'' + pure $ deliveryJobScope newMember where initialStatus = case msgScope_ of Just (MSMember _) -> GSMemPendingReview @@ -3068,10 +3165,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = messageError "x.grp.mem.intro ignored: member already exists" Left _ | useRelays' gInfo -> do - -- owner key must only come from link data, not from relay intro + -- role + key are owner-authoritative (roster); an intro establishes neither - a privileged + -- claim is created at the channel default with no key until the owner-signed roster confirms it + defaultRole <- unknownMemberRole gInfo let memInfo' = case memInfo of MemberInfo mId mRole v p _ - | mRole == GROwner -> MemberInfo mId mRole v p Nothing + | mRole >= GRMember -> MemberInfo mId defaultRole v p Nothing _ -> memInfo void $ withStore $ \db -> createIntroReMember db cxt user gInfo memInfo' memRestrictions | otherwise -> do @@ -3141,28 +3240,241 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = chatV = vr cxt `peerConnChatVersion` mcvr withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode - xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg@RcvMessage {msgSigned} brokerTs + -- rollback defense (channels): apply an owner-signed role/removal only at a version >= the persisted + -- roster_version (not the batch-constant gInfo, which a relay can stale by reordering events in one + -- batch), then advance it in the same transaction; a strictly lower version is a replay and is ignored. + -- Only an owner sender may advance it: a non-owner signed event is rejected by the action that follows, + -- but must not bump roster_version first, or every later owner roster at a lower version is dropped. + applyAtRosterVersion :: GroupInfo -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope) + applyAtRosterVersion gInfo sender rosterVer_ action + | not (useRelays' gInfo) = action + | otherwise = case rosterVer_ of + Nothing -> action + Just _ | memberRole' sender /= GROwner -> action + Just v -> do + accept <- withStore' $ \db -> do + cur <- getGroupRosterVersion db gInfo + let fresh = maybe True (v >=) cur + when fresh $ setGroupRosterVersion db gInfo v + pure fresh + if accept + then action + else messageWarning "x.grp.mem: roster version not newer than current, ignoring" $> Nothing + + xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) + xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole memberKey_ rosterVer_ msg@RcvMessage {msgSigned} brokerTs | membershipMemId == memId = - let gInfo' = gInfo {membership = membership {memberRole = memRole}} - in changeMemberRole gInfo' membership $ RGEUserRole memRole - | otherwise = - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case - Right member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole - Left _ -> messageError "x.grp.mem.role with unknown member ID" $> Nothing + applyAtRosterVersion gInfo m rosterVer_ $ + let gInfo' = gInfo {membership = membership {memberRole = memRole}} + in changeMemberRole gInfo' membership False (\db -> updateGroupMemberRole db user membership memRole) $ RGEUserRole memRole + | otherwise = applyAtRosterVersion gInfo m rosterVer_ $ do + defaultRole <- unknownMemberRole gInfo + -- an owner-signed event with a key TOFU-creates an unknown member only for a roster role; else a plain lookup + let allowCreate = useRelays' gInfo && senderRole == GROwner && isRosterRole memRole && isJust memberKey_ + withStore' (\db -> runExceptT $ getCreateUnknownGMByMemberId db cxt user gInfo memId (nameFromMemberId memId) defaultRole allowCreate) >>= \case + Right (Just (member, created)) + -- just created (keyless, and allowCreate ensured the event carries its key): pin key + role + | created, Just (MemberKey pubKey) <- memberKey_ -> + let gEvent = RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole + in changeMemberRole gInfo member created (\db -> void $ applyMemberKeyRole db member pubKey memRole) gEvent + -- known member: apply the role (its key is established via roster/intro; the event's key is ignored) + | otherwise -> + let gEvent = RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole + in changeMemberRole gInfo member created (\db -> updateGroupMemberRole db user member memRole) gEvent + -- in relay groups the roster may deliver role update for previously-unknown privileged members + _ | useRelays' gInfo -> pure Nothing + | otherwise -> messageError "x.grp.mem.role with unknown member ID" $> Nothing where GroupMember {memberId = membershipMemId} = membership - changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} gEvent + -- applyMember writes the change (role, or role + pinned key for a freshly TOFU-created member); + -- the delivery scope (relay forwarding) is computed on the pre-change role + changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} created applyMember gEvent | senderRole < maximum ([GRAdmin, fromRole, memRole] :: [GroupMemberRole]) = messageError "x.grp.mem.role with insufficient member permissions" $> Nothing + | useRelays' gInfo && (isRosterRole memRole || isRosterRole fromRole) && senderRole /= GROwner = + messageError "x.grp.mem.role: only the owner can change member, moderator and admin roles in relay groups" $> Nothing + -- a forwarded role event the roster already applied is a no-op; suppress it. + -- a just-created member is keyless here, so fall through to pin its owner-attested key. + | useRelays' gInfo && not created && fromRole == memRole = pure $ memberEventDeliveryScope member | otherwise = do - withStore' $ \db -> updateGroupMemberRole db user member memRole + withStore' applyMember (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) groupMsgToView cInfo ci toView CEvtMemberRole {user, groupInfo = gInfo'', byMember = m', member = member {memberRole = memRole}, fromRole, toRole = memRole, msgSigned} pure $ memberEventDeliveryScope member + -- The header only starts the transfer; the roster is applied and the version bumped only at + -- blob completion, so a withheld or corrupted blob leaves the last good roster intact. + -- fromMember is the relay that delivered THIS roster copy (the owner on a relay receiving directly, + -- a relay on a member receiving a forward); author is the owner who signed it. + xGrpRoster :: GroupInfo -> GroupMember -> GroupMember -> GroupRoster -> VerifiedMsg e -> Maybe SharedMsgId -> UTCTime -> CM (Maybe DeliveryJobScope) + xGrpRoster gInfo fromMember author GroupRoster {version = newVer, fileInv = InlineFileInvitation {fileSize, fileDigest}} verifiedMsg sharedMsgId_ brokerTs + -- only an owner may sign a roster; otherwise a relay could route it as a member whose key it controls + | memberRole' author /= GROwner = messageError "x.grp.roster: not signed by an owner" $> Nothing + | fileSize > maxGroupRosterBytes = messageError "x.grp.roster: roster blob size exceeds limit" $> Nothing + | otherwise = case verifiedMsg of + -- unreachable: XGrpRoster is in requiresSignature, so withVerifiedMsg rejected unsigned + VMUnsigned _ -> pure Nothing + VMSigned _ sm _ -> case sharedMsgId_ of + Nothing -> Nothing <$ messageWarning "x.grp.roster: missing shared message id" + Just sharedMsgId -> do + -- per-source pending version (THIS relay's own in-flight transfer), not a single group slot + pendingVer_ <- withStore' $ \db -> getRosterTransferVersion db gInfo (groupMemberId' fromMember) + -- accept a version not below BOTH applied and this source's pending (>=, Nothing below 0): a preceding + -- signed event may have already advanced rosterVersion to this blob's version; a lower one is a downgrade. + if newVer `notBelowRoster` rosterVersion gInfo && newVer `notBelowRoster` pendingVer_ + then startRosterTransfer sm sharedMsgId + else pure Nothing + where + startRosterTransfer sm sharedMsgId = do + -- supersede THIS source's own in-flight transfer (older version or a restart); other relays' transfers are independent + cleanupRosterTransfer gInfo (groupMemberId' fromMember) + let relayHdr = if isUserGrpFwdRelay gInfo then Just sm else Nothing + chSize <- asks $ fileChunkSize . config + let rosterFInv = FileInvitation {fileName = "roster", fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Just IFMSent, fileDescr = Nothing} + -- transfer record + its scratch file in one transaction (file owned by the transfer, keyed per source) + rft@RcvFileTransfer {fileId} <- withStore $ \db -> do + transferId <- liftIO $ createRosterTransfer db gInfo (groupMemberId' fromMember) newVer fileDigest (groupMemberId' author) brokerTs relayHdr + createRosterRcvFile db userId gInfo fromMember transferId sharedMsgId rosterFInv (Just IFMSent) (fromIntegral chSize) + -- accept the chat-item-free file before chunk 1 (FIFO before it) so chunk 1 isn't rejected on RFSNew + -- transient scratch file (consumed into roster_blob, then deleted): temp folder, not the user's files folder / Downloads + tmpDir <- lift getChatTempDirectory + rosterTs <- liftIO getCurrentTime + let GroupInfo {groupId = gId} = gInfo + rosterFile = "roster_" <> show gId <> "_" <> show (groupMemberId' fromMember) <> "_" <> formatTime defaultTimeLocale "%Y%m%d_%H%M%S" rosterTs + filePath <- getRcvFilePath fileId (Just tmpDir) rosterFile False + withStore' $ \db -> startRcvInlineFT db user rft filePath (Just IFMSent) + pure Nothing + + -- Roster version comparison treating Nothing (un-materialized) as below 0. Non-strict (>=) so a relay + -- accepts the owner's blob at the version a preceding signed event already advanced rosterVersion to. + notBelowRoster :: VersionRoster -> Maybe VersionRoster -> Bool + notBelowRoster v = maybe True (v >=) + + -- Blob arrived: verify the owner-attested digest over the plaintext and guard against + -- downgrade before applying; on a relay, ack the owner and re-serve to members. + rosterCompletion :: GroupInfo -> RcvFileTransfer -> CM () + rosterCompletion gInfo RcvFileTransfer {fileId, fileStatus} = + withStore' (\db -> getRosterTransfer db fileId) >>= \case + -- defensive: the file always has its transfer (created together, deleted together) + Nothing -> lift (closeFileHandle fileId rcvFiles) >> forM_ (rosterFilePath fileStatus) removeFsFile + Just RcvRosterTransfer {rosterTransferId = transferId, rosterTransferVersion = pendingVer, rosterTransferDigest = pendingDigest, rosterTransferOwnerGMId = ownerGMId, rosterTransferBrokerTs = rosterBrokerTs, rosterTransferHeader = header_} -> do + owner_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberById db cxt user ownerGMId) + blob <- readAssembledRoster + let isRelay = isUserGrpFwdRelay gInfo + ackErr err = do + cleanupRosterTransferById transferId + when isRelay $ forM_ owner_ $ \owner -> sendRosterAck gInfo owner pendingVer (Just err) + if FD.FileDigest (LC.sha512Hash (LB.fromStrict blob)) /= pendingDigest + then ackErr "relay could not verify the roster blob" + else case parseAll rosterBlobP blob of + Left _ -> ackErr "relay could not parse the roster blob" + Right entries -> case owner_ of + Nothing -> cleanupRosterTransferById transferId + Just author -> do + defaultRole <- unknownMemberRole gInfo + -- gate against the persisted roster_version inside the apply transaction: a roster from another + -- relay (or a preceding signed event) may already have advanced it past this one; a stale + -- completion (e.g. relay1 sent v5 then v6, relay2's v5 completes after v6) is rejected. + results_ <- withStore $ \db -> do + cur <- liftIO $ getGroupRosterVersion db gInfo + if maybe False (pendingVer <) cur + then pure Nothing + else do + res <- processRosterEntries db gInfo defaultRole (validateGroupRoster entries) + liftIO $ setGroupLiveRoster db gInfo pendingVer ownerGMId rosterBrokerTs header_ blob + pure (Just res) + cleanupRosterTransferById transferId + forM_ results_ $ \results -> do + emitRosterResults gInfo author rosterBrokerTs results + -- ack while setting up (own status accepted/acknowledged); a serving (active) relay must not ack broadcasts. + when (isRelay && (relayOwnStatus gInfo == Just RSAccepted || relayOwnStatus gInfo == Just RSAcknowledgedRoster)) $ do + sendRosterAck gInfo author pendingVer Nothing + withStore' $ \db -> void $ updateRelayOwnStatusFromTo db gInfo RSAccepted RSAcknowledgedRoster + where + rosterFilePath = \case + RFSAccepted p -> Just p + RFSConnected p -> Just p + RFSComplete p -> Just p + _ -> Nothing + readAssembledRoster = case rosterFilePath fileStatus of + Just fp -> readAt fp + Nothing -> throwChatError $ CEInternalError "roster file not in progress" + readAt fp = lift (toFSFilePath fp) >>= liftIO . B.readFile + + -- TOFU-apply an owner-signed (key, role) to a resolved member: pin the key if absent; for a keyed + -- member keep the trusted key (Left = reject a different one), else update the role. Right + -- (Just (member-at-new-role, fromRole)) when the role changed, Right Nothing when already current. + applyMemberKeyRole :: DB.Connection -> GroupMember -> C.PublicKeyEd25519 -> GroupMemberRole -> IO (Either MemberId (Maybe (GroupMember, GroupMemberRole))) + applyMemberKeyRole db m pubKey role = case memberPubKey m of + Just k + | k /= pubKey -> pure (Left (memberId' m)) + | memberRole' m == role -> pure (Right Nothing) + | otherwise -> updateGroupMemberRole db user m role $> Right (Just (m {memberRole = role}, memberRole' m)) + Nothing -> setGroupMemberKeyRole db m pubKey role $> Right (Just (m {memberRole = role}, memberRole' m)) + + -- TOFU apply: pin each member's key on first use, then update roles. + processRosterEntries :: DB.Connection -> GroupInfo -> GroupMemberRole -> [RosterMember] -> ExceptT StoreError IO ([MemberId], [(GroupMember, GroupMemberRole, Bool)]) + processRosterEntries db gInfo defaultRole entries = do + let rosterIds = map (\RosterMember {memberId} -> memberId) entries + (cs, as) <- foldrM applyRosterEntry ([], []) entries + currentPriv <- liftIO $ getGroupRosterMembers db cxt user gInfo + reverted <- liftIO $ fmap catMaybes $ forM currentPriv $ \m -> + if memberId' m `notElem` rosterIds + then updateGroupMemberRole db user m defaultRole $> Just ((m :: GroupMember) {memberRole = defaultRole}, memberRole' m, False) + else pure Nothing + pure (cs, as <> reverted) + where + -- entry-level failure (StoreError or IO exception) is muted; the entry is dropped + applyRosterEntry RosterMember {memberId, key = MemberKey pubKey, role} (cs, as) = + ( getCreateUnknownGMByMemberId db cxt user gInfo memberId (nameFromMemberId memberId) defaultRole True >>= \case + Nothing -> pure (cs, as) + Just (m, created) -> liftIO (applyMemberKeyRole db m pubKey role) >>= \case + Left mid -> pure (mid : cs, as) + Right Nothing -> pure (cs, as) + Right (Just (rm, fromR)) -> pure (cs, (rm, fromR, created) : as) + ) + `catchAllErrors` \_ -> pure (cs, as) + + emitRosterResults :: GroupInfo -> GroupMember -> UTCTime -> ([MemberId], [(GroupMember, GroupMemberRole, Bool)]) -> CM () + emitRosterResults gInfo author rosterBrokerTs (conflicts, applied) = do + forM_ conflicts $ \mid' -> + messageWarning $ "x.grp.roster: member key conflict, keeping trusted key, memberId=" <> safeDecodeUtf8 (strEncode mid') + forM_ applied $ \(member, fromRole, created) -> + unless created $ createItems member fromRole + where + createItems member fromRole = do + let toRole = memberRole' member + gEvent = RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) toRole + (gInfo', author', scopeInfo) <- mkGroupChatScope gInfo author + ci <- createChatItem user (CDGroupRcv gInfo' scopeInfo author') False (CIRcvGroupEvent gEvent) Nothing (Just MSSVerified) (Just rosterBrokerTs) + toView $ CEvtNewChatItems user [ci] + toView CEvtMemberRole {user, groupInfo = gInfo', byMember = author', member, fromRole, toRole, msgSigned = Just MSSVerified} + + sendRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM () + sendRosterAck gInfo owner ackVer err = void $ sendGroupMessage' user gInfo [owner] (XGrpRosterAck ackVer err) + + xGrpRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM () + xGrpRosterAck gInfo m ackVer err = do + relay_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupRelayByGMId db (groupMemberId' m)) + case relay_ of + Just relay@GroupRelay {relayStatus = RSAccepted} -> case err of + Nothing + | rosterVersion gInfo == Just ackVer -> do + (relay', gLink) <- withStore $ \db -> do + relay' <- liftIO $ updateRelayStatus db relay RSAcknowledgedRoster + gLink <- getGroupLink db user gInfo + pure (relay', gLink) + setGroupLinkDataAsync user gInfo gLink + toView $ CEvtGroupRelayUpdated user gInfo m relay' + | otherwise -> messageWarning "x.grp.roster.ack: stale version, awaiting ack for the current roster" + Just e -> do + relay' <- withStore' $ \db -> updateRelayStatusFromTo db relay RSAccepted RSRejected + toView $ CEvtGroupRelayUpdated user gInfo m relay' + messageError $ "x.grp.roster.ack: relay could not save roster, marked rejected: " <> e + _ -> pure () + checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = when (memberRole < GRAdmin || memberRole < memRole) $ throwChatError (CEGroupContactRole localDisplayName) @@ -3207,11 +3519,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do + xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) + xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId - then checkRole membership $ do + then applyAtRosterVersion gInfo m rosterVer_ $ checkRole membership $ do deleteGroupLinkIfExists user gInfo -- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False @@ -3223,7 +3535,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = deleteMemberItem msg gInfo RGEUserDeleted toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned pure $ Just DJSGroup {jobSpec = DJRelayRemoved} - else + else applyAtRosterVersion gInfo m rosterVer_ $ withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Left _ -> do messageError "x.grp.mem.del with unknown member ID" @@ -3474,7 +3786,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = processForwardedMsg :: VerifiedMsg 'Json -> Maybe GroupMember -> CM () processForwardedMsg verifiedMsg author_ = do rcvMsg_ <- saveGroupFwdRcvMsg user gInfo m author_ verifiedMsg brokerTs - forM_ rcvMsg_ $ \rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} -> case event of + forM_ rcvMsg_ $ \rcvMsg@RcvMessage {sharedMsgId_, chatMsgEvent = ACME _ event} -> case event of XMsgNew mc -> void $ memberCanSend author_ scope $ newGroupContentMessage gInfo author_ mc rcvMsg msgTs True where @@ -3489,13 +3801,14 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XInfo p -> withAuthor XInfo_ $ \author -> void $ xInfoMember gInfo author p rcvMsg msgTs XGrpRelayNew rl -> withAuthor XGrpRelayNew_ $ \author -> void $ xGrpRelayNew gInfo author rl XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs - XGrpMemRole memId memRole -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo author memId memRole rcvMsg msgTs + XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo author memId memRole memberKey rosterVer rcvMsg msgTs XGrpMemRestrict memId memRestrictions -> withAuthor XGrpMemRestrict_ $ \author -> void $ xGrpMemRestrict gInfo author memId memRestrictions rcvMsg msgTs - XGrpMemDel memId withMessages -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo author memId withMessages verifiedMsg rcvMsg msgTs True + XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> withAuthor XGrpInfo_ $ \author -> void $ xGrpInfo gInfo author p' rcvMsg msgTs XGrpPrefs ps' -> withAuthor XGrpPrefs_ $ \author -> void $ xGrpPrefs gInfo author ps' rcvMsg + XGrpRoster gr -> withAuthor XGrpRoster_ $ \author -> void $ xGrpRoster gInfo m author gr verifiedMsg sharedMsgId_ msgTs _ -> messageError $ "x.grp.msg.forward: unsupported forwarded event " <> T.pack (show $ toCMEventTag event) where withAuthor :: CMEventTag e -> (GroupMember -> CM ()) -> CM () @@ -3515,12 +3828,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just sm@SignedMsg {chatBinding, signatures, signedBody} | GroupMember {memberPubKey = Just pubKey, memberId} <- member -> case chatBinding of - CBGroup -> - let prefix = smpEncode chatBinding <> bindingData - bindingData = case groupKeys gInfo of - Just GroupKeys {publicGroupId} -> smpEncode (publicGroupId, memberId) - Nothing -> smpEncode (memberId, pubKey) -- forward compatibility for verifying signed messages in p2p groups - in signed MSSVerified <$ guard (all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) + CBGroup + | Just GroupKeys {publicGroupId} <- groupKeys gInfo -> + signed MSSVerified <$ guard (verifyGroupSig pubKey publicGroupId memberId signatures signedBody) + | otherwise -> + let prefix = smpEncode chatBinding <> smpEncode (memberId, pubKey) -- forward compatibility for verifying signed messages in p2p groups + in signed MSSVerified <$ guard (all (\case (MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) _ -> signed MSSSignedNoKey <$ guard signatureOptional | otherwise -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag) where @@ -3782,10 +4095,15 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do bucketSize <- asks $ deliveryBucketSize . config senders <- withStore' $ \db -> fmap catMaybes . forM senderGMIds $ \sId -> - fmap eitherToMaybe . runExceptT $ do + fmap (join . eitherToMaybe) . runExceptT $ do sender <- getNonRemovedMemberById db cxt user sId - vec <- getMemberRelationsVector db sender - pure (sender, vec) + -- owners are already known to every member (group link + owner-intro in introduceInChannel), + -- so we never disseminate their profile (redundant, and races with joins re-announcing the owner) + if memberRole' sender == GROwner + then pure Nothing + else do + vec <- getMemberRelationsVector db sender + pure $ Just (sender, vec) let missingSenders = length senderGMIds - length senders when (missingSenders > 0) $ logInfo $ "delivery job " <> tshow jobId <> ": " <> tshow missingSenders <> " senders missing; skipping their profile prepend" @@ -3795,13 +4113,8 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do if null senders then pure (body, [], [], []) else do - -- Skip role > GRMember (mirrors xGrpMemNew gate). - -- TODO [relays] public groups: revisit if mods/admins are introduced via this sidecar. - let (encoderErrs, validLabeled) = - partitionEithers - [ (\bs -> (s, bs)) <$> encodeMemberNew (vr cxt) gInfo s - | (s, _) <- senders, memberRole' s <= GRMember - ] + -- all members' profiles disseminate; privileged key/role come from the roster, not here + let (encoderErrs, validLabeled) = partitionEithers [(\bs -> (s, bs)) <$> encodeMemberNew (vr cxt) gInfo s | (s, _) <- senders] (extBody', inBody, overflowLabeled, large1) = batchProfilesWithBody maxEncodedMsgLength body validLabeled (overflowBatches', large2) = batchProfiles maxEncodedMsgLength overflowLabeled packerErrs = [ChatError (CEInternalError $ "oversized profile element for member " <> show (groupMemberId' s)) | s <- large1 <> large2] diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index f3c3e20e47..223fe492a9 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -48,13 +48,14 @@ import Data.Time.Clock (UTCTime) import Data.Time.Clock.System (systemToUTCTime, utcToSystemTime) import Data.Type.Equality import Data.Typeable (Typeable) -import Data.Word (Word32) +import Data.Word (Word16, Word32) import Simplex.Chat.Badges (LocalBadge) import Simplex.Chat.Call import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared +import qualified Simplex.FileTransfer.Description as FD import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) import Simplex.Messaging.Compression (Compressed, compress1, decompress1, decompressedSize) @@ -84,12 +85,13 @@ import Simplex.Messaging.Version hiding (version) -- 16 - support short link data (2025-06-10) -- 17 - allow host voice messages during member approval regardless of group voice setting (2026-02-10) -- 18 - relay web capabilities (2026-05-31) +-- 19 - group roster (2026-06-18) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 18 +currentChatVersion = VersionChat 19 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -160,6 +162,11 @@ memberSupportVoiceVersion = VersionChat 17 relayWebCapVersion :: VersionChat relayWebCapVersion = VersionChat 18 +-- owner-signed roster (promoted members/moderators/admins) and the relay roster-ack handshake; +-- a relay below this version is published without the handshake (it can't ack a roster) +groupRosterVersion :: VersionChat +groupRosterVersion = VersionChat 19 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion @@ -373,6 +380,36 @@ data GrpMsgForward = GrpMsgForward } deriving (Eq, Show) +-- | Owner-signed roster header for the privileged (moderator/admin/member) set; owners +-- are not included, their keys come from the link. The member list itself is not +-- here: it is sent as a binary blob over the inline file transfer, and this header +-- carries only its inline-file invitation (size + owner-attested digest). +data GroupRoster = GroupRoster + { version :: VersionRoster, + fileInv :: InlineFileInvitation + } + deriving (Eq, Show) + +-- | Lean always-inline file invitation for the roster blob, carried in the signed +-- header. The digest authenticates the unsigned blob; integrity is entirely the digest. +data InlineFileInvitation = InlineFileInvitation + { fileSize :: Integer, + fileDigest :: FD.FileDigest + } + deriving (Eq, Show) + +data RosterMember = RosterMember + { memberId :: MemberId, + key :: MemberKey, -- trust-on-first-use pinned per memberId + role :: GroupMemberRole, + privileges :: Word16 -- reserved: serialized as 0, parsed and ignored in v1 + } + deriving (Eq, Show) + +-- RosterMember is binary-only: it rides in the roster blob, never in a JSON message. +instance Encoding RosterMember where + smpEncode RosterMember {memberId, key, role, privileges} = smpEncode (memberId, key, role, privileges) + smpP = RosterMember <$> smpP <*> smpP <*> smpP <*> smpP instance Encoding FwdSender where smpEncode = \case @@ -439,6 +476,11 @@ data MsgSigning = MsgSigning encodeChatBinding :: ChatBinding -> ByteString -> ByteString encodeChatBinding cb bindingData = smpEncode cb <> bindingData +signChatMsgBody :: MsgSigning -> ByteString -> SignedMsg +signChatMsgBody MsgSigning {bindingTag, bindingData, keyRef, privKey} msgBody = + let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody) + in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig L.:| [], signedBody = msgBody} + data ChatMsgEvent (e :: MsgEncoding) where XMsgNew :: MsgContainer -> ChatMsgEvent 'Json XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json @@ -452,7 +494,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json XInfo :: Profile -> ChatMsgEvent 'Json XContact :: {profile :: Profile, contactReqId :: Maybe XContactId, welcomeMsgId :: Maybe SharedMsgId, requestMsg :: Maybe (SharedMsgId, MsgContent)} -> ChatMsgEvent 'Json - XMember :: {profile :: Profile, newMemberId :: MemberId, newMemberKey :: MemberKey} -> ChatMsgEvent 'Json + XMember :: {profile :: Profile, newMemberId :: MemberId, newMemberKey :: MemberKey, viaRelay :: Maybe MemberId} -> ChatMsgEvent 'Json XDirectDel :: ChatMsgEvent 'Json XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json XGrpAcpt :: MemberId -> ChatMsgEvent 'Json @@ -471,16 +513,18 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json XGrpMemFwd :: MemberInfo -> IntroInvitation -> ChatMsgEvent 'Json XGrpMemInfo :: MemberId -> Profile -> ChatMsgEvent 'Json - XGrpMemRole :: MemberId -> GroupMemberRole -> ChatMsgEvent 'Json + XGrpMemRole :: MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> ChatMsgEvent 'Json XGrpMemRestrict :: MemberId -> MemberRestrictions -> ChatMsgEvent 'Json XGrpMemCon :: MemberId -> ChatMsgEvent 'Json XGrpMemConAll :: MemberId -> ChatMsgEvent 'Json -- TODO not implemented - XGrpMemDel :: MemberId -> Bool -> ChatMsgEvent 'Json + XGrpMemDel :: MemberId -> Bool -> Maybe VersionRoster -> ChatMsgEvent 'Json XGrpLeave :: ChatMsgEvent 'Json XGrpDel :: ChatMsgEvent 'Json XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json XGrpPrefs :: GroupPreferences -> ChatMsgEvent 'Json XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> Maybe MsgScope -> ChatMsgEvent 'Json + XGrpRoster :: GroupRoster -> ChatMsgEvent 'Json + XGrpRosterAck :: VersionRoster -> Maybe Text -> ChatMsgEvent 'Json XGrpMsgForward :: GrpMsgForward -> ChatMessage 'Json -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json @@ -524,6 +568,7 @@ isForwardedGroupMsg ev = case ev of XGrpDel -> True XGrpInfo _ -> True XGrpPrefs _ -> True + XGrpRoster _ -> True _ -> False data MsgReaction = MREmoji {emoji :: MREmojiChar} | MRUnknown {tag :: Text, json :: J.Object} @@ -792,6 +837,8 @@ data MsgMention = MsgMention {memberId :: MemberId} newtype MsgMentions = MsgMentions (Map MemberName MsgMention) deriving (Eq, Show) +$(JQ.deriveJSON defaultJSON ''InlineFileInvitation) + $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MCL") ''MsgChatLink) $(JQ.deriveJSON defaultJSON ''LinkOwnerSig) @@ -892,6 +939,28 @@ maxCompressedMsgLength = 13380 maxDecompressedMsgLength :: Int maxDecompressedMsgLength = 65536 +-- Defensive entry-count bound for the roster blob parser (rosterBlobP) and the +-- promotion cap over the promoted (member/moderator/admin) set. +maxGroupRosterSize :: Int +maxGroupRosterSize = 256 + +-- Receive-side byte bound: reject an owner-signed header whose claimed fileSize exceeds what +-- maxGroupRosterSize entries can occupy (128 B/entry is a generous worst case), before a file is created. +-- 128 B/entry ~ memberId + X.509 Ed25519 key (44 B) + role + privileges + 1-byte length prefixes (~2x the ~65 B typical). +maxGroupRosterBytes :: Integer +maxGroupRosterBytes = fromIntegral maxGroupRosterSize * 128 + +-- The byte sequence the owner-signed digest is computed over and verified against +-- before parsing. Word16 count (smpEncodeList's 1-byte count is too small for the future cap). +encodeRosterBlob :: [RosterMember] -> ByteString +encodeRosterBlob ms = smpEncode (fromIntegral (length ms) :: Word16) <> B.concat (map smpEncode ms) + +rosterBlobP :: A.Parser [RosterMember] +rosterBlobP = do + n <- fromIntegral <$> smpP @Word16 + when (n > maxGroupRosterSize) $ fail "roster: too many entries" + A.count n smpP + -- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead) -- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008 maxEncodedInfoLength :: Int @@ -1028,6 +1097,8 @@ data CMEventTag (e :: MsgEncoding) where XGrpInfo_ :: CMEventTag 'Json XGrpPrefs_ :: CMEventTag 'Json XGrpDirectInv_ :: CMEventTag 'Json + XGrpRoster_ :: CMEventTag 'Json + XGrpRosterAck_ :: CMEventTag 'Json XGrpMsgForward_ :: CMEventTag 'Json XInfoProbe_ :: CMEventTag 'Json XInfoProbeCheck_ :: CMEventTag 'Json @@ -1088,6 +1159,8 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpInfo_ -> "x.grp.info" XGrpPrefs_ -> "x.grp.prefs" XGrpDirectInv_ -> "x.grp.direct.inv" + XGrpRoster_ -> "x.grp.roster" + XGrpRosterAck_ -> "x.grp.roster.ack" XGrpMsgForward_ -> "x.grp.msg.forward" XInfoProbe_ -> "x.info.probe" XInfoProbeCheck_ -> "x.info.probe.check" @@ -1149,6 +1222,8 @@ instance StrEncoding ACMEventTag where "x.grp.info" -> XGrpInfo_ "x.grp.prefs" -> XGrpPrefs_ "x.grp.direct.inv" -> XGrpDirectInv_ + "x.grp.roster" -> XGrpRoster_ + "x.grp.roster.ack" -> XGrpRosterAck_ "x.grp.msg.forward" -> XGrpMsgForward_ "x.info.probe" -> XInfoProbe_ "x.info.probe.check" -> XInfoProbeCheck_ @@ -1196,7 +1271,7 @@ toCMEventTag msg = case msg of XGrpMemInv _ _ -> XGrpMemInv_ XGrpMemFwd _ _ -> XGrpMemFwd_ XGrpMemInfo _ _ -> XGrpMemInfo_ - XGrpMemRole _ _ -> XGrpMemRole_ + XGrpMemRole {} -> XGrpMemRole_ XGrpMemRestrict _ _ -> XGrpMemRestrict_ XGrpMemCon _ -> XGrpMemCon_ XGrpMemConAll _ -> XGrpMemConAll_ @@ -1206,6 +1281,8 @@ toCMEventTag msg = case msg of XGrpInfo _ -> XGrpInfo_ XGrpPrefs _ -> XGrpPrefs_ XGrpDirectInv {} -> XGrpDirectInv_ + XGrpRoster _ -> XGrpRoster_ + XGrpRosterAck {} -> XGrpRosterAck_ XGrpMsgForward {} -> XGrpMsgForward_ XInfoProbe _ -> XInfoProbe_ XInfoProbeCheck _ -> XInfoProbeCheck_ @@ -1264,6 +1341,7 @@ requiresSignature = \case XGrpMemRestrict_ -> True XGrpLeave_ -> True XGrpRelayNew_ -> True + XGrpRoster_ -> True XInfo_ -> True _ -> False @@ -1332,7 +1410,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do reqContent <- opt "content" let requestMsg = (,) <$> reqMsgId <*> reqContent pure XContact {profile, contactReqId, welcomeMsgId, requestMsg} - XMember_ -> XMember <$> p "profile" <*> p "newMemberId" <*> p "newMemberKey" + XMember_ -> XMember <$> p "profile" <*> p "newMemberId" <*> p "newMemberKey" <*> opt "viaRelay" XDirectDel_ -> pure XDirectDel XGrpInv_ -> XGrpInv <$> p "groupInvitation" XGrpAcpt_ -> XGrpAcpt <$> p "memberId" @@ -1354,16 +1432,18 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" XGrpMemFwd_ -> XGrpMemFwd <$> p "memberInfo" <*> p "memberIntro" XGrpMemInfo_ -> XGrpMemInfo <$> p "memberId" <*> p "profile" - XGrpMemRole_ -> XGrpMemRole <$> p "memberId" <*> p "role" + XGrpMemRole_ -> XGrpMemRole <$> p "memberId" <*> p "role" <*> opt "memberKey" <*> opt "rosterVersion" XGrpMemRestrict_ -> XGrpMemRestrict <$> p "memberId" <*> p "memberRestrictions" XGrpMemCon_ -> XGrpMemCon <$> p "memberId" XGrpMemConAll_ -> XGrpMemConAll <$> p "memberId" - XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") + XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") <*> opt "rosterVersion" XGrpLeave_ -> pure XGrpLeave XGrpDel_ -> pure XGrpDel XGrpInfo_ -> XGrpInfo <$> p "groupProfile" XGrpPrefs_ -> XGrpPrefs <$> p "groupPreferences" XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" <*> opt "scope" + XGrpRoster_ -> XGrpRoster <$> (GroupRoster <$> p "version" <*> p "fileInv") + XGrpRosterAck_ -> XGrpRosterAck <$> p "version" <*> opt "error" XGrpMsgForward_ -> do fwdSender <- opt "memberId" >>= \case Just memberId -> FwdMember memberId . fromMaybe "" <$> opt "memberName" @@ -1405,7 +1485,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId] XInfo profile -> o ["profile" .= profile] XContact {profile, contactReqId, welcomeMsgId, requestMsg} -> o $ ("contactReqId" .=? contactReqId) $ ("welcomeMsgId" .=? welcomeMsgId) $ ("msgId" .=? (fst <$> requestMsg)) $ ("content" .=? (snd <$> requestMsg)) $ ["profile" .= profile] - XMember {profile, newMemberId, newMemberKey} -> o ["profile" .= profile, "newMemberId" .= newMemberId, "newMemberKey" .= newMemberKey] + XMember {profile, newMemberId, newMemberKey, viaRelay} -> o $ ("viaRelay" .=? viaRelay) ["profile" .= profile, "newMemberId" .= newMemberId, "newMemberKey" .= newMemberKey] XDirectDel -> JM.empty XGrpInv groupInv -> o ["groupInvitation" .= groupInv] XGrpAcpt memId -> o ["memberId" .= memId] @@ -1426,16 +1506,18 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] XGrpMemFwd memInfo memIntro -> o ["memberInfo" .= memInfo, "memberIntro" .= memIntro] XGrpMemInfo memId profile -> o ["memberId" .= memId, "profile" .= profile] - XGrpMemRole memId role -> o ["memberId" .= memId, "role" .= role] + XGrpMemRole memId role memberKey rosterVersion -> o $ ("memberKey" .=? memberKey) $ ("rosterVersion" .=? rosterVersion) ["memberId" .= memId, "role" .= role] XGrpMemRestrict memId memRestrictions -> o ["memberId" .= memId, "memberRestrictions" .= memRestrictions] XGrpMemCon memId -> o ["memberId" .= memId] XGrpMemConAll memId -> o ["memberId" .= memId] - XGrpMemDel memId messages -> o $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId] + XGrpMemDel memId messages rosterVersion -> o $ ("rosterVersion" .=? rosterVersion) $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId] XGrpLeave -> JM.empty XGrpDel -> JM.empty XGrpInfo p -> o ["groupProfile" .= p] XGrpPrefs p -> o ["groupPreferences" .= p] XGrpDirectInv connReq content scope -> o $ ("content" .=? content) $ ("scope" .=? scope) ["connReq" .= connReq] + XGrpRoster GroupRoster {version, fileInv} -> o ["version" .= version, "fileInv" .= fileInv] + XGrpRosterAck version err -> o $ ("error" .=? err) ["version" .= version] XGrpMsgForward GrpMsgForward {fwdSender, fwdBrokerTs} msg -> o $ encodeFwdSender fwdSender ["msg" .= msg, "msgTs" .= fwdBrokerTs] where encodeFwdSender = \case diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index e5ebf8e2bd..2953c1de1f 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -149,7 +149,7 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 6cb8e39bbc..dee72731a8 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -31,12 +31,19 @@ module Simplex.Chat.Store.Files getSharedMsgIdByFileId, getFileIdBySharedMsgId, getGroupFileIdBySharedMsgId, + getGroupRcvFileId, + getGroupRosterFileInfo, + deleteGroupRosterFile, + getRosterTransferFile, + deleteRosterTransferFile, + getRcvFileLastChunkNo, getDirectFileIdBySharedMsgId, getChatRefByFileId, lookupChatRefByFileId, updateSndFileStatus, createRcvFileTransfer, createRcvGroupFileTransfer, + createRosterRcvFile, createRcvStandaloneFileTransfer, appendRcvFD, getRcvFileDescrByRcvFileId, @@ -321,6 +328,64 @@ getGroupFileIdBySharedMsgId db userId groupId sharedMsgId = |] (userId, groupId, sharedMsgId) +-- Resolve the in-flight received group inline file for a chunk: read its file_type by shared_msg_id +-- (LIMIT 1 is safe -- all files sharing a shared_msg_id share a type), then look up by type: a roster +-- file is scoped to its source relay (every relay re-serves the owner's same shared_msg_id, so the source +-- disambiguates), a normal file is by shared_msg_id. Nothing => no in-flight transfer (orphaned chunk). +getGroupRcvFileId :: DB.Connection -> UserId -> Int64 -> GroupMemberId -> SharedMsgId -> IO (Maybe Int64) +getGroupRcvFileId db userId groupId fromMemberId sharedMsgId = do + fileType_ <- getFileType + case fileType_ of + Just FTRoster -> + maybeFirstRow fromOnly $ + DB.query db (rcvFileIdQ <> " AND r.group_member_id = ?") (userId, groupId, sharedMsgId, FTRoster, fromMemberId) + Just FTNormal -> + maybeFirstRow fromOnly $ + DB.query db rcvFileIdQ (userId, groupId, sharedMsgId, FTNormal) + Nothing -> pure Nothing + where + getFileType = + maybeFirstRow fromOnly $ + DB.query db "SELECT file_type FROM files WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? LIMIT 1" (userId, groupId, sharedMsgId) + rcvFileIdQ = + [sql| + SELECT f.file_id FROM files f + JOIN rcv_files r ON r.file_id = f.file_id + WHERE f.user_id = ? AND f.group_id = ? AND f.shared_msg_id = ? AND f.file_type = ? + |] + +-- The roster scratch file for a transfer (for fs/handle cleanup before deleting the transfer). +-- A transfer owns exactly one file (created together in one transaction), so this is single-valued. +getRosterTransferFile :: DB.Connection -> Int64 -> IO (Maybe (Int64, Maybe FilePath)) +getRosterTransferFile db transferId = + maybeFirstRow id $ DB.query db "SELECT file_id, file_path FROM files WHERE roster_transfer_id = ?" (Only transferId) + +-- Deletes a transfer's file row; rcv_files and rcv_file_chunks cascade on the FK. +deleteRosterTransferFile :: DB.Connection -> Int64 -> IO () +deleteRosterTransferFile db transferId = + DB.execute db "DELETE FROM files WHERE roster_transfer_id = ?" (Only transferId) + +-- For roster-file cleanup keyed on the group (not a chat item): every matching file_id and its on-disk +-- path, so the caller evicts the handle and removes the file for each — delete-all like deleteGroupRosterFile. +getGroupRosterFileInfo :: DB.Connection -> UserId -> Int64 -> IO [(Int64, Maybe FilePath)] +getGroupRosterFileInfo db userId groupId = + DB.query + db + "SELECT file_id, file_path FROM files WHERE user_id = ? AND group_id = ? AND file_type = ?" + (userId, groupId, FTRoster) + +-- Deletes the roster files row; rcv_files and rcv_file_chunks cascade on the FK. +deleteGroupRosterFile :: DB.Connection -> UserId -> Int64 -> IO () +deleteGroupRosterFile db userId groupId = + DB.execute db "DELETE FROM files WHERE user_id = ? AND group_id = ? AND file_type = ?" (userId, groupId, FTRoster) + +-- The highest stored chunk number, or Nothing if no partial chunks exist (used to decide +-- whether an arriving chunk 1 is a re-driven transfer that must reset). +getRcvFileLastChunkNo :: DB.Connection -> RcvFileTransfer -> IO (Maybe Integer) +getRcvFileLastChunkNo db RcvFileTransfer {fileId} = + maybeFirstRow fromOnly $ + DB.query db "SELECT chunk_number FROM rcv_file_chunks WHERE file_id = ? ORDER BY chunk_number DESC LIMIT 1" (Only fileId) + getDirectFileIdBySharedMsgId :: DB.Connection -> User -> Contact -> SharedMsgId -> ExceptT StoreError IO Int64 getDirectFileIdBySharedMsgId db User {userId} Contact {contactId} sharedMsgId = ExceptT . firstRow fromOnly (SEFileIdNotFoundBySharedMsgId sharedMsgId) $ @@ -379,10 +444,10 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs) - pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing, cryptoArgs = Nothing} + pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, fileType = FTNormal, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing, cryptoArgs = Nothing} -createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> Maybe GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer -createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gName} m_ f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do +createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> Maybe GroupMember -> FileType -> Maybe SharedMsgId -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer +createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gName} m_ fileType sharedMsgId_ f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do currentTs <- liftIO getCurrentTime rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ @@ -394,15 +459,34 @@ createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gNam fileId <- liftIO $ do DB.execute db - "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" - (userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, currentTs, currentTs) + "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)" + (userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, fileType, sharedMsgId_, currentTs, currentTs) insertedRowId db liftIO $ DB.execute db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, grpMemberId_, rfdId, currentTs, currentTs) - pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = grpMemberId_, cryptoArgs = Nothing} + pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, fileType, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = grpMemberId_, cryptoArgs = Nothing} + +-- Roster scratch file owned by a per-source transfer: group_member_id is the delivering relay (so chunk +-- streams from different relays are distinct files), roster_transfer_id links to the metadata record. +createRosterRcvFile :: DB.Connection -> UserId -> GroupInfo -> GroupMember -> Int64 -> SharedMsgId -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer +createRosterRcvFile db userId GroupInfo {groupId} src@GroupMember {localDisplayName = senderName} transferId sharedMsgId f@FileInvitation {fileName, fileSize, fileConnReq, fileInline} rcvFileInline chunkSize = do + currentTs <- liftIO getCurrentTime + let grpMemberId_ = groupMemberId' src + fileId <- liftIO $ do + DB.execute + db + "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, roster_transfer_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, FPSMP, FTRoster) :. (sharedMsgId, transferId, currentTs, currentTs)) + insertedRowId db + liftIO $ + DB.execute + db + "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, grpMemberId_, currentTs, currentTs) + pure RcvFileTransfer {fileId, xftpRcvFile = Nothing, fileInvitation = f, fileStatus = RFSNew, fileType = FTRoster, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = Just grpMemberId_, cryptoArgs = Nothing} createRcvStandaloneFileTransfer :: DB.Connection -> UserId -> CryptoFile -> Int64 -> Word32 -> ExceptT StoreError IO Int64 createRcvStandaloneFileTransfer db userId (CryptoFile filePath cfArgs_) fileSize chunkSize = do @@ -548,7 +632,7 @@ getRcvFileTransfer_ db userId fileId = do SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, - r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN contacts cs ON cs.contact_id = f.contact_id @@ -562,9 +646,9 @@ getRcvFileTransfer_ db userId fileId = do where rcvFileTransfer :: Maybe RcvFileDescr -> - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe BoolInt) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, BoolInt, BoolInt) :. Only (Maybe ContactName) -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe BoolInt) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, BoolInt, BoolInt) :. (Maybe ContactName, FileType) -> ExceptT StoreError IO RcvFileTransfer - rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, BI agentRcvFileDeleted, BI userApprovedRelays) :. Only groupName_) = + rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, BI agentRcvFileDeleted, BI userApprovedRelays) :. (groupName_, fileType)) = case contactName_ <|> memberName_ <|> groupName_ <|> standaloneName_ of Nothing -> throwError $ SERcvFileInvalid fileId Just name -> @@ -582,7 +666,7 @@ getRcvFileTransfer_ db userId fileId = do let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} cryptoArgs = CFArgs <$> fileKey <*> fileNonce xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_ - in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} + in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, fileType, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} filePath = case filePath_ of Nothing -> throwError $ SERcvFileInvalid fileId Just fp -> pure fp @@ -678,7 +762,15 @@ createRcvFileChunk db RcvFileTransfer {fileId, fileInvitation = FileInvitation { currentTs <- getCurrentTime DB.execute db - "INSERT OR REPLACE INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) VALUES (?,?,?,?,?)" + [sql| + INSERT INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) + VALUES (?,?,?,?,?) + ON CONFLICT (file_id, chunk_number) DO UPDATE SET + chunk_agent_msg_id = excluded.chunk_agent_msg_id, + chunk_stored = 0, + created_at = excluded.created_at, + updated_at = excluded.updated_at + |] (fileId, chunkNo, msgId, currentTs, currentTs) pure status where diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 2ea3fa9b84..60531cc1dc 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -67,6 +67,9 @@ module Simplex.Chat.Store.Groups getGroupMembersByIndexes, getSupportScopeMembersByIndexes, getGroupModerators, + getGroupRosterMembers, + getGroupAdminsMods, + getGroupOnlyMembers, getGroupOwners, getGroupRelayMembers, getGroupMembersForExpiration, @@ -84,7 +87,19 @@ module Simplex.Chat.Store.Groups getGroupRelayById, getGroupRelayByGMId, getGroupRelays, - getConnectedGroupRelays, + getPublishableGroupRelays, + setGroupRosterVersion, + getGroupRosterVersion, + getGroupRoster, + RcvRosterTransfer (..), + createRosterTransfer, + getRosterTransferVersion, + getRosterTransferId, + getRosterTransfer, + setGroupLiveRoster, + deleteRosterTransfer, + deleteGroupRosterTransfers, + setGroupMemberKeyRole, createRelayForOwner, getCreateRelayForMember, createRelayConnection, @@ -173,6 +188,7 @@ module Simplex.Chat.Store.Groups createLinkOwnerMember, updatePreparedChannelMember, updateUnknownMemberAnnounced, + updateRosterMemberAnnounced, updateUserMemberProfileSentAt, setGroupCustomData, setGroupUIThemes, @@ -216,6 +232,8 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) +import qualified Simplex.FileTransfer.Description as FD +import Simplex.Messaging.Encoding (smpDecode, smpEncode) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import Simplex.Messaging.Agent.Store.Entity (DBEntityId) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -359,6 +377,7 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId) Nothing -> (Nothing, Nothing, Nothing) fullGroupPreferences = mergeGroupPreferences groupPreferences + rosterVersion0 = if useRelays then Just (VersionRoster 0) else Nothing currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do @@ -389,11 +408,11 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays INSERT INTO groups (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, - root_priv_key, root_pub_key, member_priv_key, public_member_count) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + root_priv_key, root_pub_key, member_priv_key, public_member_count, roster_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) - :. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_) + :. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_, rosterVersion0) ) insertedRowId db let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys @@ -420,6 +439,7 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays chatItemTTL = Nothing, uiThemes = Nothing, groupSummary = GroupSummary {currentMembers = 1, publicMemberCount = publicMemberCount_}, + rosterVersion = rosterVersion0, customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, @@ -497,6 +517,7 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti chatItemTTL = Nothing, uiThemes = Nothing, groupSummary = GroupSummary {currentMembers = 2, publicMemberCount = Nothing}, + rosterVersion = Nothing, customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, @@ -1215,10 +1236,42 @@ getGroupModerators db cxt user@User {userId, userContactId} GroupInfo {groupId} (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)") (userId, groupId, userContactId, GRModerator, GRAdmin, GROwner) +-- The full roster set - members, moderators and admins - excluding owners (link-anchored) and +-- left/removed members. For the privileged subset only use getGroupAdminsMods; for plain members +-- only use getGroupOnlyMembers. +getGroupRosterMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupRosterMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)") + (userId, groupId, userContactId, GRMember, GRModerator, GRAdmin) + +-- Moderators and admins only (excluding owners and plain members) - the set introduced to a +-- joiner; plain members are learned from the roster blob, not via introductions. +getGroupAdminsMods :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupAdminsMods db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?)") + (userId, groupId, userContactId, GRModerator, GRAdmin) + +getGroupOnlyMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupOnlyMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ?") + (userId, groupId, userContactId, GRMember) + getGroupOwners :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] getGroupOwners db cxt user@User {userId, userContactId} GroupInfo {groupId} = do - ts <- getCurrentTime - map (toContactMember ts cxt user) + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) <$> DB.query db (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ?") @@ -1373,21 +1426,30 @@ getGroupRelays db GroupInfo {groupId} = (groupRelayQuery <> " WHERE gr.group_id = ?") (Only groupId) -getConnectedGroupRelays :: DB.Connection -> GroupInfo -> IO [GroupRelay] -getConnectedGroupRelays db GroupInfo {groupId} = - map toGroupRelay - <$> DB.query - db - ( groupRelayQuery - <> " " - <> [sql| - JOIN group_members m ON m.group_member_id = gr.group_member_id - WHERE gr.group_id = ? - AND m.member_status = ? - AND gr.relay_status IN (?,?) - |] - ) - (groupId, GSMemConnected, RSAccepted, RSActive) +-- Relays whose link is published to subscribers: acked relays (RSAcknowledgedRoster/RSActive) plus +-- pre-roster relays at RSAccepted (below groupRosterVersion, they can't ack a roster), gated by the +-- relay's negotiated version read from its member connection. +getPublishableGroupRelays :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupRelay] +getPublishableGroupRelays db cxt user gInfo@GroupInfo {groupId} = do + relays <- + map toGroupRelay + <$> DB.query + db + ( groupRelayQuery + <> " " + <> [sql| + JOIN group_members m ON m.group_member_id = gr.group_member_id + WHERE gr.group_id = ? + AND m.member_status = ? + AND gr.relay_status IN (?,?,?) + |] + ) + (groupId, GSMemConnected, RSAccepted, RSAcknowledgedRoster, RSActive) + members <- getGroupRelayMembers db cxt user gInfo + pure [gr | gr@GroupRelay {groupMemberId} <- relays, m <- members, groupMemberId' m == groupMemberId, publishable gr m] + where + publishable GroupRelay {relayStatus} m = + relayStatus /= RSAccepted || not (m `supportsVersion` groupRosterVersion) groupRelayQuery :: Query groupRelayQuery = @@ -1405,6 +1467,149 @@ toGroupRelay ((groupRelayId, groupMemberId, chatRelayId, address, displayName, f relayCap = RelayCapabilities {webDomain} in GroupRelay {groupRelayId, groupMemberId, userChatRelay, relayStatus, relayLink, relayCap} +setGroupRosterVersion :: DB.Connection -> GroupInfo -> VersionRoster -> IO () +setGroupRosterVersion db GroupInfo {groupId} v = do + currentTs <- getCurrentTime + DB.execute db "UPDATE groups SET roster_version = ?, updated_at = ? WHERE group_id = ?" (v, currentTs, groupId) + +-- Persisted roster version (the gate baseline; the in-memory gInfo copy is batch-constant and stale on reorder). +getGroupRosterVersion :: DB.Connection -> GroupInfo -> IO (Maybe VersionRoster) +getGroupRosterVersion db GroupInfo {groupId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT roster_version FROM groups WHERE group_id = ?" (Only groupId) + +-- The live roster header a relay re-serves to joiners, with the completed blob served alongside it +-- (both are written together at completion, so the blob is present whenever the header is). +getGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg, Maybe ByteString)) +getGroupRoster db GroupInfo {groupId} = + (>>= toRoster) + <$> maybeFirstRow + id + ( DB.query + db + "SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob FROM groups WHERE group_id = ?" + (Only groupId) + ) + where + toRoster (Just ownerGMId, Just brokerTs, Just cb, Just (Binary sigsBs), Just (Binary body), blob_) = + (\sigs -> (ownerGMId, brokerTs, SignedMsg cb sigs body, (\(Binary b) -> b) <$> blob_)) <$> eitherToMaybe (smpDecode sigsBs) + toRoster _ = Nothing + +-- A per-source in-flight roster transfer, keyed (group_id, from_member_id): replaces the single +-- roster_pending_* slot, so two relays serving one member can't share a chunk stream. The signed-header +-- columns are relay-only (NULL on members), promoted to the live roster_msg_* on groups at completion. +createRosterTransfer :: DB.Connection -> GroupInfo -> GroupMemberId -> VersionRoster -> FD.FileDigest -> GroupMemberId -> UTCTime -> Maybe SignedMsg -> IO Int64 +createRosterTransfer db GroupInfo {groupId} fromMemberId v digest ownerGMId brokerTs sm_ = do + -- one in-flight transfer per (group, source): drop any prior row from this source so the INSERT can't hit + -- the UNIQUE constraint even if the caller's fs/handle cleanup was skipped (the scratch file would then leak + -- until group delete, but the transfer never gets stuck). Normally cleanupRosterTransfer ran first. + DB.execute db "DELETE FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId) + DB.execute + db + [sql| + INSERT INTO rcv_roster_transfers + (group_id, from_member_id, roster_version, roster_digest, sending_owner_gm_id, broker_ts, + roster_msg_chat_binding, roster_msg_signatures, roster_msg_body) + VALUES (?,?,?,?,?,?,?,?,?) + |] + ( (groupId, fromMemberId, v, Binary (FD.unFileDigest digest), ownerGMId, brokerTs) + :. ((\SignedMsg {chatBinding} -> chatBinding) <$> sm_, (\SignedMsg {signatures} -> Binary (smpEncode signatures)) <$> sm_, (\SignedMsg {signedBody} -> Binary signedBody) <$> sm_) + ) + insertedRowId db + +getRosterTransferVersion :: DB.Connection -> GroupInfo -> GroupMemberId -> IO (Maybe VersionRoster) +getRosterTransferVersion db GroupInfo {groupId} fromMemberId = + maybeFirstRow fromOnly $ + DB.query db "SELECT roster_version FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId) + +getRosterTransferId :: DB.Connection -> GroupInfo -> GroupMemberId -> IO (Maybe Int64) +getRosterTransferId db GroupInfo {groupId} fromMemberId = + maybeFirstRow fromOnly $ + DB.query db "SELECT roster_transfer_id FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId) + +-- An in-flight received roster transfer (a rcv_roster_transfers row joined to its scratch file), read at +-- completion. The header is the relay's re-serve SignedMsg -- present only on a serving relay (NULL on a +-- member, whose live roster_msg_* stay NULL so it never re-serves). +data RcvRosterTransfer = RcvRosterTransfer + { rosterTransferId :: Int64, + rosterTransferVersion :: VersionRoster, + rosterTransferDigest :: FD.FileDigest, + rosterTransferOwnerGMId :: GroupMemberId, + rosterTransferBrokerTs :: UTCTime, + rosterTransferHeader :: Maybe SignedMsg + } + deriving (Show) + +-- The in-flight transfer for a received roster file (joined via files.roster_transfer_id), with its +-- relay-only signed header. Read at completion to apply, promote into the live roster, and ack. +getRosterTransfer :: DB.Connection -> Int64 -> IO (Maybe RcvRosterTransfer) +getRosterTransfer db fileId = + (>>= toTransfer) + <$> maybeFirstRow + id + ( DB.query + db + [sql| + SELECT t.roster_transfer_id, t.roster_version, t.roster_digest, t.sending_owner_gm_id, t.broker_ts, + t.roster_msg_chat_binding, t.roster_msg_signatures, t.roster_msg_body + FROM rcv_roster_transfers t + JOIN files f ON f.roster_transfer_id = t.roster_transfer_id + WHERE f.file_id = ? + |] + (Only fileId) + ) + where + toTransfer (tId, v, Binary d, ownerGMId, brokerTs, cb_, sigs_, body_) = + Just + RcvRosterTransfer + { rosterTransferId = tId, + rosterTransferVersion = v, + rosterTransferDigest = FD.FileDigest d, + rosterTransferOwnerGMId = ownerGMId, + rosterTransferBrokerTs = brokerTs, + rosterTransferHeader = sm_ + } + where + sm_ = case (cb_, sigs_, body_) of + (Just cb, Just (Binary sigsBs), Just (Binary body)) -> + (\sigs -> SignedMsg cb sigs body) <$> eitherToMaybe (smpDecode sigsBs) + _ -> Nothing + +-- Write the single live roster on groups from a completed transfer's values (header NULL on a member, +-- so its live roster_msg_* stay NULL and it never re-serves; only relays re-serve). +setGroupLiveRoster :: DB.Connection -> GroupInfo -> VersionRoster -> GroupMemberId -> UTCTime -> Maybe SignedMsg -> ByteString -> IO () +setGroupLiveRoster db GroupInfo {groupId} v ownerGMId brokerTs sm_ blob = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE groups SET + roster_version = ?, roster_blob = ?, + roster_sending_owner_gm_id = ?, roster_broker_ts = ?, + roster_msg_chat_binding = ?, roster_msg_signatures = ?, roster_msg_body = ?, + updated_at = ? + WHERE group_id = ? + |] + ( (v, Binary blob, ownerGMId, brokerTs) + :. ((\SignedMsg {chatBinding} -> chatBinding) <$> sm_, (\SignedMsg {signatures} -> Binary (smpEncode signatures)) <$> sm_, (\SignedMsg {signedBody} -> Binary signedBody) <$> sm_, currentTs, groupId) + ) + +-- Delete one in-flight transfer row (its files/rcv_files/rcv_file_chunks are removed separately, with +-- the on-disk file). Caller removes the fs file + cached handle first. +deleteRosterTransfer :: DB.Connection -> Int64 -> IO () +deleteRosterTransfer db transferId = + DB.execute db "DELETE FROM rcv_roster_transfers WHERE roster_transfer_id = ?" (Only transferId) + +-- All in-flight transfers for a group (group delete). +deleteGroupRosterTransfers :: DB.Connection -> Int64 -> IO () +deleteGroupRosterTransfers db groupId = + DB.execute db "DELETE FROM rcv_roster_transfers WHERE group_id = ?" (Only groupId) + +setGroupMemberKeyRole :: DB.Connection -> GroupMember -> C.PublicKeyEd25519 -> GroupMemberRole -> IO () +setGroupMemberKeyRole db GroupMember {groupMemberId} pubKey role = do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_members SET member_pub_key = ?, member_role = ?, updated_at = ? WHERE group_member_id = ?" (pubKey, role, currentTs, groupMemberId) + createRelayForOwner :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupMember createRelayForOwner db cxt gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {relayProfile = RelayProfile {displayName}} = do currentTs <- liftIO getCurrentTime @@ -1713,9 +1918,9 @@ getRelayServedGroups db cxt User {userId, userContactId} = do <$> DB.query db ( groupInfoQuery - <> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?)" + <> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?, ?)" ) - (userId, userContactId, RSAccepted, RSActive) + (userId, userContactId, RSAccepted, RSAcknowledgedRoster, RSActive) getRelayPublishableGroups :: DB.Connection -> User -> IO [(Int64, B64UrlByteString, Maybe PublicGroupAccess)] getRelayPublishableGroups db User {userId, userContactId} = @@ -3182,11 +3387,11 @@ createLinkOwnerMember db cxt user@User {userId, userContactId} GroupInfo {groupI where VersionRange minV maxV = vr cxt --- member_pub_key is not updated here — introduced members are owners --- whose keys are loaded from link data (trusted out-of-band). --- Updating from an in-band message would allow a compromised relay to substitute keys. +-- Intro refreshes only profile / status / peer version. Role and key stay owner-authoritative +-- (the owner-signed roster for members/moderators/admins, link data for owners), so taking either from +-- an in-band relayed intro would let a compromised relay substitute them. updatePreparedChannelMember :: DB.Connection -> StoreCxt -> User -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember -updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do +updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {v, profile} = do _ <- updateMemberProfile db cxt user member profile currentTs <- liftIO getCurrentTime liftIO $ @@ -3194,14 +3399,13 @@ updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupM db [sql| UPDATE group_members - SET member_role = ?, - member_status = ?, + SET member_status = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ? |] - (memberRole, GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId) + (GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId) getGroupMemberById db cxt user groupMemberId where VersionRange minV maxV = maybe memberChatVRange fromChatVRange v @@ -3233,6 +3437,30 @@ updateUnknownMemberAnnounced db cxt user@User {userId} invitingMember unknownMem VersionRange minV maxV = maybe memberChatVRange fromChatVRange v memberPubKey_ = (\(MemberKey k) -> k) <$> memberKey +-- Like updateUnknownMemberAnnounced but preserves member_role and member_pub_key +-- (roster-established for moderators/admins; the dissemination carries only the profile). +updateRosterMemberAnnounced :: DB.Connection -> StoreCxt -> User -> GroupMember -> GroupMember -> MemberInfo -> GroupMemberStatus -> ExceptT StoreError IO GroupMember +updateRosterMemberAnnounced db cxt user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {v, profile} status = do + _ <- updateMemberProfile db cxt user unknownMember profile + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + UPDATE group_members + SET member_category = ?, + member_status = ?, + invited_by_group_member_id = ?, + peer_chat_min_version = ?, + peer_chat_max_version = ?, + updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + ((GCPostMember, status, groupMemberId' invitingMember) :. (minV, maxV, currentTs, userId, groupMemberId)) + getGroupMemberById db cxt user groupMemberId + where + VersionRange minV maxV = maybe memberChatVRange fromChatVRange v + updateUserMemberProfileSentAt :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO () updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs = DB.execute diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index cf12db7ec1..644d73137d 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -238,10 +238,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent msgSigning_ encodeMessage case encodeMessage (SharedMsgId sharedMsgId) of ECMLarge -> pure $ Left SELargeMsg ECMEncoded msgBody -> do - let signedMsg_ = signBody <$> msgSigning_ - signBody MsgSigning {bindingTag, bindingData, keyRef, privKey} = - let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody) - in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig :| [], signedBody = msgBody} + let signedMsg_ = (`signChatMsgBody` msgBody) <$> msgSigning_ createdAt <- getCurrentTime DB.execute db @@ -584,9 +581,9 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, msgS CDChannelRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ -> (Just $ Just userMemberId == memberId, memberId) -createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Bool -> UTCTime -> UTCTime -> IO ChatItemId -createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ hasLink itemTs = - createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False hasLink itemTs Nothing Nothing +createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Bool -> Maybe MsgSigStatus -> UTCTime -> UTCTime -> IO ChatItemId +createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ hasLink msgSigned itemTs = + createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False hasLink itemTs Nothing msgSigned where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 20acf0b602..4b814d0434 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -37,6 +37,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260529_delivery_job_senders import Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain +import Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -73,7 +74,8 @@ schemaMigrations = ("20260529_delivery_job_senders", m20260529_delivery_job_senders, Just down_m20260529_delivery_job_senders), ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), - ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain) + ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260602_group_roster.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260602_group_roster.hs new file mode 100644 index 0000000000..892b2c70da --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260602_group_roster.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260602_group_roster :: Text +m20260602_group_roster = + [r| +ALTER TABLE groups ADD COLUMN roster_version BIGINT; +ALTER TABLE groups ADD COLUMN roster_msg_body BYTEA; +ALTER TABLE groups ADD COLUMN roster_msg_chat_binding TEXT; +ALTER TABLE groups ADD COLUMN roster_msg_signatures BYTEA; +ALTER TABLE groups ADD COLUMN roster_sending_owner_gm_id BIGINT; +ALTER TABLE groups ADD COLUMN roster_broker_ts TIMESTAMPTZ; +ALTER TABLE groups ADD COLUMN roster_blob BYTEA; + +CREATE TABLE rcv_roster_transfers( + roster_transfer_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + from_member_id BIGINT NOT NULL REFERENCES group_members ON DELETE CASCADE, + roster_version BIGINT NOT NULL, + roster_digest BYTEA NOT NULL, + sending_owner_gm_id BIGINT NOT NULL, + broker_ts TIMESTAMPTZ NOT NULL, + roster_msg_body BYTEA, + roster_msg_chat_binding TEXT, + roster_msg_signatures BYTEA, + created_at TEXT NOT NULL DEFAULT (now()), + updated_at TEXT NOT NULL DEFAULT (now()) +); +CREATE UNIQUE INDEX idx_rcv_roster_transfers_group_id_from_member_id ON rcv_roster_transfers(group_id, from_member_id); +CREATE INDEX idx_rcv_roster_transfers_from_member_id ON rcv_roster_transfers(from_member_id); + +ALTER TABLE files ADD COLUMN shared_msg_id BYTEA; +ALTER TABLE files ADD COLUMN file_type TEXT NOT NULL DEFAULT 'normal'; +ALTER TABLE files ADD COLUMN roster_transfer_id BIGINT; +CREATE INDEX idx_files_group_id_shared_msg_id ON files(group_id, shared_msg_id); +CREATE INDEX idx_files_roster_transfer_id ON files(roster_transfer_id); +|] + +down_m20260602_group_roster :: Text +down_m20260602_group_roster = + [r| +DROP INDEX idx_files_roster_transfer_id; +DROP INDEX idx_files_group_id_shared_msg_id; +ALTER TABLE files DROP COLUMN roster_transfer_id; +ALTER TABLE files DROP COLUMN file_type; +ALTER TABLE files DROP COLUMN shared_msg_id; + +DROP INDEX idx_rcv_roster_transfers_from_member_id; +DROP INDEX idx_rcv_roster_transfers_group_id_from_member_id; +DROP TABLE rcv_roster_transfers; + +ALTER TABLE groups DROP COLUMN roster_blob; +ALTER TABLE groups DROP COLUMN roster_broker_ts; +ALTER TABLE groups DROP COLUMN roster_sending_owner_gm_id; +ALTER TABLE groups DROP COLUMN roster_msg_signatures; +ALTER TABLE groups DROP COLUMN roster_msg_chat_binding; +ALTER TABLE groups DROP COLUMN roster_msg_body; +ALTER TABLE groups DROP COLUMN roster_version; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 89cddd48e5..861224ff56 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -752,7 +752,10 @@ CREATE TABLE test_chat_schema.files ( file_crypto_key bytea, file_crypto_nonce bytea, note_folder_id bigint, - redirect_file_id bigint + redirect_file_id bigint, + shared_msg_id bytea, + file_type text DEFAULT 'normal'::text NOT NULL, + roster_transfer_id bigint ); @@ -977,9 +980,16 @@ CREATE TABLE test_chat_schema.groups ( public_member_count bigint, relay_request_retries bigint DEFAULT 0 NOT NULL, relay_request_delay bigint DEFAULT 0 NOT NULL, - relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 01:00:00+01'::timestamp with time zone NOT NULL, + relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL, relay_inactive_at timestamp with time zone, - relay_sent_web_domain text + relay_sent_web_domain text, + roster_version bigint, + roster_msg_body bytea, + roster_msg_chat_binding text, + roster_msg_signatures bytea, + roster_sending_owner_gm_id bigint, + roster_broker_ts timestamp with time zone, + roster_blob bytea ); @@ -1206,6 +1216,34 @@ CREATE TABLE test_chat_schema.rcv_files ( +CREATE TABLE test_chat_schema.rcv_roster_transfers ( + roster_transfer_id bigint NOT NULL, + group_id bigint NOT NULL, + from_member_id bigint NOT NULL, + roster_version bigint NOT NULL, + roster_digest bytea NOT NULL, + sending_owner_gm_id bigint NOT NULL, + broker_ts timestamp with time zone NOT NULL, + roster_msg_body bytea, + roster_msg_chat_binding text, + roster_msg_signatures bytea, + created_at text DEFAULT now() NOT NULL, + updated_at text DEFAULT now() NOT NULL +); + + + +ALTER TABLE test_chat_schema.rcv_roster_transfers ALTER COLUMN roster_transfer_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME test_chat_schema.rcv_roster_transfers_roster_transfer_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + CREATE TABLE test_chat_schema.received_probes ( received_probe_id bigint NOT NULL, contact_id bigint, @@ -1739,6 +1777,11 @@ ALTER TABLE ONLY test_chat_schema.rcv_files +ALTER TABLE ONLY test_chat_schema.rcv_roster_transfers + ADD CONSTRAINT rcv_roster_transfers_pkey PRIMARY KEY (roster_transfer_id); + + + ALTER TABLE ONLY test_chat_schema.received_probes ADD CONSTRAINT received_probes_pkey PRIMARY KEY (received_probe_id); @@ -2272,10 +2315,18 @@ CREATE INDEX idx_files_group_id ON test_chat_schema.files USING btree (group_id) +CREATE INDEX idx_files_group_id_shared_msg_id ON test_chat_schema.files USING btree (group_id, shared_msg_id); + + + CREATE INDEX idx_files_redirect_file_id ON test_chat_schema.files USING btree (redirect_file_id); +CREATE INDEX idx_files_roster_transfer_id ON test_chat_schema.files USING btree (roster_transfer_id); + + + CREATE INDEX idx_files_user_id ON test_chat_schema.files USING btree (user_id); @@ -2448,6 +2499,14 @@ CREATE INDEX idx_rcv_files_group_member_id ON test_chat_schema.rcv_files USING b +CREATE INDEX idx_rcv_roster_transfers_from_member_id ON test_chat_schema.rcv_roster_transfers USING btree (from_member_id); + + + +CREATE UNIQUE INDEX idx_rcv_roster_transfers_group_id_from_member_id ON test_chat_schema.rcv_roster_transfers USING btree (group_id, from_member_id); + + + CREATE INDEX idx_received_probes_contact_id ON test_chat_schema.received_probes USING btree (contact_id); @@ -3133,6 +3192,16 @@ ALTER TABLE ONLY test_chat_schema.rcv_files +ALTER TABLE ONLY test_chat_schema.rcv_roster_transfers + ADD CONSTRAINT rcv_roster_transfers_from_member_id_fkey FOREIGN KEY (from_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.rcv_roster_transfers + ADD CONSTRAINT rcv_roster_transfers_group_id_fkey FOREIGN KEY (group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE CASCADE; + + + ALTER TABLE ONLY test_chat_schema.received_probes ADD CONSTRAINT received_probes_contact_id_fkey FOREIGN KEY (contact_id) REFERENCES test_chat_schema.contacts(contact_id) ON DELETE CASCADE; diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 78838c507f..c9dd316aee 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -160,6 +160,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260529_delivery_job_senders import Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain +import Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -319,7 +320,8 @@ schemaMigrations = ("20260529_delivery_job_senders", m20260529_delivery_job_senders, Just down_m20260529_delivery_job_senders), ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), - ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain) + ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260602_group_roster.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260602_group_roster.hs new file mode 100644 index 0000000000..d68fea3a56 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260602_group_roster.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260602_group_roster :: Query +m20260602_group_roster = + [sql| +ALTER TABLE groups ADD COLUMN roster_version INTEGER; +ALTER TABLE groups ADD COLUMN roster_msg_body BLOB; +ALTER TABLE groups ADD COLUMN roster_msg_chat_binding TEXT; +ALTER TABLE groups ADD COLUMN roster_msg_signatures BLOB; +ALTER TABLE groups ADD COLUMN roster_sending_owner_gm_id INTEGER; +ALTER TABLE groups ADD COLUMN roster_broker_ts TEXT; +ALTER TABLE groups ADD COLUMN roster_blob BLOB; + +CREATE TABLE rcv_roster_transfers( + roster_transfer_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + from_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, + roster_version INTEGER NOT NULL, + roster_digest BLOB NOT NULL, + sending_owner_gm_id INTEGER NOT NULL, + broker_ts TEXT NOT NULL, + roster_msg_body BLOB, + roster_msg_chat_binding TEXT, + roster_msg_signatures BLOB, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +) STRICT; +CREATE UNIQUE INDEX idx_rcv_roster_transfers_group_id_from_member_id ON rcv_roster_transfers(group_id, from_member_id); +CREATE INDEX idx_rcv_roster_transfers_from_member_id ON rcv_roster_transfers(from_member_id); + +ALTER TABLE files ADD COLUMN shared_msg_id BLOB; +ALTER TABLE files ADD COLUMN file_type TEXT NOT NULL DEFAULT 'normal'; +ALTER TABLE files ADD COLUMN roster_transfer_id INTEGER; +CREATE INDEX idx_files_group_id_shared_msg_id ON files(group_id, shared_msg_id); +CREATE INDEX idx_files_roster_transfer_id ON files(roster_transfer_id); +|] + +down_m20260602_group_roster :: Query +down_m20260602_group_roster = + [sql| +DROP INDEX idx_files_roster_transfer_id; +DROP INDEX idx_files_group_id_shared_msg_id; +ALTER TABLE files DROP COLUMN roster_transfer_id; +ALTER TABLE files DROP COLUMN file_type; +ALTER TABLE files DROP COLUMN shared_msg_id; + +DROP INDEX idx_rcv_roster_transfers_from_member_id; +DROP INDEX idx_rcv_roster_transfers_group_id_from_member_id; +DROP TABLE rcv_roster_transfers; + +ALTER TABLE groups DROP COLUMN roster_blob; +ALTER TABLE groups DROP COLUMN roster_broker_ts; +ALTER TABLE groups DROP COLUMN roster_sending_owner_gm_id; +ALTER TABLE groups DROP COLUMN roster_msg_signatures; +ALTER TABLE groups DROP COLUMN roster_msg_chat_binding; +ALTER TABLE groups DROP COLUMN roster_msg_body; +ALTER TABLE groups DROP COLUMN roster_version; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 0cac477bd5..fb6166f8c9 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -30,6 +30,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -82,6 +83,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -148,7 +150,7 @@ Query: g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -286,6 +288,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -320,6 +323,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -354,6 +358,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -537,6 +542,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -570,6 +576,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -604,6 +611,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -638,6 +646,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -1082,6 +1091,17 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) +Query: + SELECT t.roster_transfer_id, t.roster_version, t.roster_digest, t.sending_owner_gm_id, t.broker_ts, + t.roster_msg_chat_binding, t.roster_msg_signatures, t.roster_msg_body + FROM rcv_roster_transfers t + JOIN files f ON f.roster_transfer_id = t.roster_transfer_id + WHERE f.file_id = ? + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH t USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE chat_items SET user_id = ?, updated_at = ? @@ -1182,6 +1202,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -1217,6 +1238,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -1266,8 +1288,8 @@ Query: INSERT INTO groups (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, - root_priv_key, root_pub_key, member_priv_key, public_member_count) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + root_priv_key, root_pub_key, member_priv_key, public_member_count, roster_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1669,7 +1691,7 @@ Query: SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, - r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN contacts cs ON cs.contact_id = f.contact_id @@ -1865,6 +1887,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -1899,6 +1922,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -1946,6 +1970,17 @@ Query: Plan: +Query: + INSERT INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) + VALUES (?,?,?,?,?) + ON CONFLICT (file_id, chunk_number) DO UPDATE SET + chunk_agent_msg_id = excluded.chunk_agent_msg_id, + chunk_stored = 0, + created_at = excluded.created_at, + updated_at = excluded.updated_at + +Plan: + Query: INSERT INTO remote_hosts (host_device_name, store_path, bind_addr, bind_iface, bind_port, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub) @@ -3708,6 +3743,15 @@ Plan: SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=?) SEARCH f USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +Query: + SELECT f.file_id FROM files f + JOIN rcv_files r ON r.file_id = f.file_id + WHERE f.user_id = ? AND f.group_id = ? AND f.shared_msg_id = ? AND f.file_type = ? + AND r.group_member_id = ? +Plan: +SEARCH f USING INDEX idx_files_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) +SEARCH r USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=? AND rowid=?) + Query: SELECT file_id, contact_id, group_id, note_folder_id FROM files @@ -4059,6 +4103,19 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET member_category = ?, + member_status = ?, + invited_by_group_member_id = ?, + peer_chat_min_version = ?, + peer_chat_max_version = ?, + updated_at = ? + WHERE user_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET member_id = ?, member_pub_key = ?, updated_at = ? @@ -4077,8 +4134,7 @@ SCAN group_members Query: UPDATE group_members - SET member_role = ?, - member_status = ?, + SET member_status = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? @@ -4783,6 +4839,14 @@ Query: Plan: +Query: + INSERT INTO rcv_roster_transfers + (group_id, from_member_id, roster_version, roster_digest, sending_owner_gm_id, broker_ts, + roster_msg_chat_binding, roster_msg_signatures, roster_msg_body) + VALUES (?,?,?,?,?,?,?,?,?) + +Plan: + Query: INSERT INTO remote_controllers (ctrl_device_name, ca_key, ca_cert, ctrl_fingerprint, id_pub, dh_priv_key, prev_dh_priv_key) @@ -5259,6 +5323,17 @@ Query: Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE groups SET + roster_version = ?, roster_blob = ?, + roster_sending_owner_gm_id = ?, roster_broker_ts = ?, + roster_msg_chat_binding = ?, roster_msg_signatures = ?, roster_msg_body = ?, + updated_at = ? + WHERE group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE msg_deliveries SET delivery_status = ?, updated_at = ? @@ -5276,6 +5351,14 @@ Query: Plan: SEARCH protocol_servers USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE rcv_file_chunks + SET chunk_stored = 1, updated_at = ? + WHERE file_id = ? AND chunk_number = ? + +Plan: +SEARCH rcv_file_chunks USING PRIMARY KEY (file_id=? AND chunk_number=?) + Query: UPDATE rcv_files SET to_receive = 1, user_approved_relays = ?, updated_at = ? @@ -5391,7 +5474,7 @@ Query: g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -5429,7 +5512,7 @@ Query: g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -5460,7 +5543,7 @@ Query: g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, @@ -5742,6 +5825,26 @@ SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +Query: + SELECT + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.group_member_id = m.group_member_id + WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?) +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN + Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, @@ -5872,11 +5975,11 @@ Query: FROM group_relays gr JOIN chat_relays cr ON cr.chat_relay_id = gr.chat_relay_id - JOIN group_members m ON m.group_member_id = gr.group_member_id - WHERE gr.group_id = ? - AND m.member_status = ? - AND gr.relay_status IN (?,?) - + JOIN group_members m ON m.group_member_id = gr.group_member_id + WHERE gr.group_id = ? + AND m.member_status = ? + AND gr.relay_status IN (?,?,?) + Plan: SEARCH gr USING INDEX idx_group_relays_group_id (group_id=?) SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) @@ -6491,6 +6594,14 @@ SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND loca SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +Query: DELETE FROM files WHERE roster_transfer_id = ? +Plan: +SEARCH files USING COVERING INDEX idx_files_roster_transfer_id (roster_transfer_id=?) +SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?) +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) +SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) + Query: DELETE FROM files WHERE user_id = ? AND contact_id = ? Plan: SEARCH files USING INDEX idx_files_contact_id (contact_id=?) @@ -6499,9 +6610,18 @@ SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) +Query: DELETE FROM files WHERE user_id = ? AND group_id = ? AND file_type = ? +Plan: +SEARCH files USING INDEX idx_files_group_id (group_id=?) +SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?) +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) +SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) + Query: DELETE FROM group_members WHERE user_id = ? AND group_id = ? Plan: SEARCH group_members USING COVERING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -6531,6 +6651,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM group_members WHERE user_id = ? AND group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) @@ -6560,6 +6681,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM groups WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_id (group_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_group_id (group_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_group_id (group_id=?) @@ -6632,6 +6754,18 @@ Query: DELETE FROM rcv_file_chunks WHERE file_id = ? Plan: SEARCH rcv_file_chunks USING COVERING INDEX idx_rcv_file_chunks_file_id (file_id=?) +Query: DELETE FROM rcv_roster_transfers WHERE group_id = ? +Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=?) + +Query: DELETE FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ? +Plan: +SEARCH rcv_roster_transfers USING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=? AND from_member_id=?) + +Query: DELETE FROM rcv_roster_transfers WHERE roster_transfer_id = ? +Plan: +SEARCH rcv_roster_transfers USING INTEGER PRIMARY KEY (rowid=?) + Query: DELETE FROM received_probes WHERE created_at <= ? Plan: SEARCH received_probes USING COVERING INDEX idx_received_probes_created_at (created_at StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) = +toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) = let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences @@ -689,7 +689,7 @@ toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayN businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers, publicMemberCount} - in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri, groupKeys} + in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupKeys} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case @@ -789,7 +789,7 @@ groupInfoQueryFields = g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index f23ea8a041..538faf8cac 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -490,6 +490,7 @@ data GroupInfo = GroupInfo uiThemes :: Maybe UIThemeEntityOverrides, customData :: Maybe CustomData, groupSummary :: GroupSummary, + rosterVersion :: Maybe VersionRoster, membersRequireAttention :: Int, viaGroupLinkUri :: Maybe ConnReqContact, groupKeys :: Maybe GroupKeys @@ -1021,6 +1022,11 @@ newtype MemberKey = MemberKey C.PublicKeyEd25519 deriving (Eq, Show) deriving newtype (StrEncoding) +-- Binary encoding for the roster blob; delegates to the Ed25519 key. +instance Encoding MemberKey where + smpEncode (MemberKey k) = smpEncode k + smpP = MemberKey <$> smpP + instance FromJSON MemberKey where parseJSON = strParseJSON "MemberKey" @@ -1542,11 +1548,38 @@ instance ToJSON InlineFileMode where toJSON = J.String . textEncode toEncoding = JE.text . textEncode +-- Discriminates ordinary chat files from the roster blob file, so the receive +-- completion / cancel paths branch on the type rather than on chat_item_id (note +-- folders and redirects also lack a chat item). +data FileType = FTNormal | FTRoster + deriving (Eq, Show) + +instance TextEncoding FileType where + textEncode = \case + FTNormal -> "normal" + FTRoster -> "roster" + textDecode = \case + "normal" -> Just FTNormal + "roster" -> Just FTRoster + _ -> Nothing + +instance FromField FileType where fromField = fromTextField_ textDecode + +instance ToField FileType where toField = toField . textEncode + +instance FromJSON FileType where + parseJSON = textParseJSON "FileType" + +instance ToJSON FileType where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + data RcvFileTransfer = RcvFileTransfer { fileId :: FileTransferId, xftpRcvFile :: Maybe XFTPRcvFile, fileInvitation :: FileInvitation, fileStatus :: RcvFileStatus, + fileType :: FileType, rcvFileInline :: Maybe InlineFileMode, senderDisplayName :: ContactName, chunkSize :: Integer, @@ -2091,6 +2124,12 @@ data StoreCxt = StoreCxt {vr :: VersionRangeChat, badgeKeys :: Map Int BBSPublic pattern VersionChat :: Word16 -> VersionChat pattern VersionChat v = Version v +-- A monotonic per-change counter, not a negotiated protocol version: Int64 rather than the Word16 of +-- Version, so a long-lived high-churn channel cannot wrap and be permanently rejected by relays (v >= cur). +newtype VersionRoster = VersionRoster Int64 + deriving (Eq, Ord, Show) + deriving newtype (FromJSON, ToJSON, FromField, ToField) + -- this newtype exists to have a concise JSON encoding of version ranges in chat protocol messages in the form of "1-2" or just "1" newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRangeChat} deriving (Eq, Show) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index c71f7ce37a..296268b58f 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -11,6 +11,7 @@ import qualified Data.ByteString.Char8 as B import Data.Text (Text) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON) import Simplex.Messaging.Util ((<$?>)) @@ -57,6 +58,12 @@ instance ToJSON GroupMemberRole where toJSON = textToJSON toEncoding = textToEncoding +-- Binary encoding for the roster blob; delegates to the canonical TextEncoding +-- (same member/moderator/admin form JSON and the DB use). GRUnknown round-trips. +instance Encoding GroupMemberRole where + smpEncode = smpEncode . textEncode + smpP = maybe (fail "bad GroupMemberRole") pure . textDecode =<< smpP + data GroupAcceptance = GAAccepted | GAPendingApproval | GAPendingReview deriving (Eq, Show) instance StrEncoding GroupAcceptance where @@ -82,6 +89,7 @@ data RelayStatus = RSNew -- only for owner | RSInvited | RSAccepted + | RSAcknowledgedRoster | RSActive | RSInactive | RSRejected @@ -92,6 +100,7 @@ relayStatusText = \case RSNew -> "new" RSInvited -> "invited" RSAccepted -> "accepted" + RSAcknowledgedRoster -> "acknowledged_roster" RSActive -> "active" RSInactive -> "inactive" RSRejected -> "rejected" @@ -101,6 +110,7 @@ instance TextEncoding RelayStatus where RSNew -> "new" RSInvited -> "invited" RSAccepted -> "accepted" + RSAcknowledgedRoster -> "acknowledged_roster" RSActive -> "active" RSInactive -> "inactive" RSRejected -> "rejected" @@ -108,6 +118,7 @@ instance TextEncoding RelayStatus where "new" -> Just RSNew "invited" -> Just RSInvited "accepted" -> Just RSAccepted + "acknowledged_roster" -> Just RSAcknowledgedRoster "active" -> Just RSActive "inactive" -> Just RSInactive "rejected" -> Just RSRejected diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 442834b244..c955ca1353 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -217,7 +217,7 @@ testCfg = shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"], testView = True, tbqSize = 16, - channelSubscriberRole = GRMember, -- starting role is GRMember to test members sending messages + channelSubscriberRole = GRObserver, confirmMigrations = MCYesUp } diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index ac9e325ed2..a32aa2d07c 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -20,13 +20,13 @@ import Control.Monad (forM_, void, when) import Data.Bifunctor (second) import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Maybe (fromMaybe, isJust, listToMaybe, maybeToList) +import Data.Maybe (fromMaybe, isJust, maybeToList) import Data.Time (UTCTime) import Data.Int (Int64) -import Data.List (intercalate, isInfixOf) +import Data.List (intercalate, isInfixOf, isSuffixOf) import qualified Data.Map.Strict as M import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), ChatLogLevel (..), defaultChatHooks) import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames) import Simplex.Chat.Markdown (parseMaybeMarkdownList) import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) @@ -288,6 +288,18 @@ chatGroupTests = do it "operator allow clears rejection and relay accepts again" testRelayAllowAcceptsAgain it "rejection on channel A does not affect unrelated channel B" testRelayDoesNotRejectUnrelatedChannel it "concurrent fresh invitations both rejected" testRelayRejectRaceConcurrentInvitations + describe "promoted members roster" $ do + it "moderator action verifies via owner-signed roster" testChannelModeratorActionViaRoster + it "removed moderator drops from the roster cache" testChannelRemovedModeratorRefreshesRoster + it "role transitions update the roster (mod <-> admin, admin -> non-roster)" testChannelRoleTransitionsUpdateRoster + it "malicious relay cannot downgrade or re-key a roster-established moderator via XGrpMemNew" testChannelRelayCannotDowngradeRosterMember + it "should add relay to channel with roster (relay caches roster before joinable)" testChannelAddRelayWithRoster + it "roster blob spanning multiple chunks reassembles" testChannelRosterMultipartReassembly + it "corrupted roster blob is rejected on digest mismatch" testChannelRosterDigestMismatchRejected + it "promoted member enters the roster and can post" testChannelPromotedMemberCanPost + it "observer cannot post until promoted" testChannelObserverCannotPost + it "promoted member re-connecting via a new relay is accepted via the roster-pinned key" testChannelPromotedMemberRejoinViaRelay + it "2 relays: multi-chunk roster reassembles per source (no stream interleaving)" testChannelRosterMultiRelayMultipart describe "channel message operations" $ do it "should update channel message" testChannelMessageUpdate it "should delete channel message" testChannelMessageDelete @@ -8676,6 +8688,20 @@ createChannel1Relay gName owner relay cath dan eve = do forM_ [cath, dan, eve] $ \member -> memberJoinChannel gName [relay] [owner] shortLink fullLink member +-- Promote a fresh channel subscriber (observer default) to member so it can post; the roster bump +-- re-serves to the other (still-unknown) subscribers, who see the change rendered by member id hash. +promoteChannelMember :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> [TestCC] -> IO () +promoteChannelMember gName owner relay member others = do + mName <- userName member + oName <- userName owner + owner ##> ("/mr #" <> gName <> " " <> mName <> " member") + owner <## ("#" <> gName <> ": you changed the role of " <> mName <> " to member (signed)") + concurrentlyN_ $ + [ relay <## ("#" <> gName <> ": " <> oName <> " changed the role of " <> mName <> " from observer to member (signed)"), + member <## ("#" <> gName <> ": " <> oName <> " changed your role from observer to member (signed)") + ] + <> [o <### [EndsWith "from observer to member (signed)"] | o <- others] + setupRelay :: TestCC -> TestCC -> IO String setupRelay owner relay = do rName <- userName relay @@ -8710,7 +8736,7 @@ prepareChannel' relayId gName owner relay = do ] owner ##> ("/show link #" <> gName) - getGroupLinks owner gName GRMember False + getGroupLinks owner gName GRObserver False createChannel2Relays :: String -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO () createChannel2Relays gName owner relay1 relay2 dan eve frank = do @@ -8741,7 +8767,7 @@ prepareChannel2Relays gName owner relay1 relay2 = do owner <## ("#" <> gName <> ": group link relays updated, current relays:") owner <### [ EndsWith ": active", - EndsWith ": accepted" + Predicate (\l -> ": invited" `isSuffixOf` l || ": accepted" `isSuffixOf` l || ": acknowledged_roster" `isSuffixOf` l) ] owner <## "group link:" void $ getTermLine owner -- consume group link line @@ -8758,7 +8784,7 @@ prepareChannel2Relays gName owner relay1 relay2 = do ] owner ##> ("/show link #" <> gName) - getGroupLinks owner gName GRMember False + getGroupLinks owner gName GRObserver False memberJoinChannel :: String -> [TestCC] -> [TestCC] -> String -> String -> TestCC -> IO () memberJoinChannel gName = memberJoinChannel' gName 1 0 0 0 @@ -8887,6 +8913,9 @@ testChannelsSenderDeduplicateOwn ps = do withNewTestChat ps "eve" eveProfile $ \eve -> do withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath and dan while the relay is online, so their buffered posts replay as members + promoteChannelMember "team" alice bob cath [dan, eve] + promoteChannelMember "team" alice bob dan [cath, eve] -- chat relay bob is offline alice #> "#team 1" @@ -8912,14 +8941,16 @@ testChannelsSenderDeduplicateOwn ps = do WithTime "#team dan> 6 [>>]" ] cath - <### [ "#team: bob introduced dan (Daniel) in the channel", + <### [ EndsWith "updated to dan", + "#team: bob introduced dan (Daniel) in the channel", WithTime "#team> 1 [>>]", WithTime "#team> 2 [>>]", WithTime "#team> 3 [>>]", WithTime "#team dan> 6 [>>]" ] dan - <### [ "#team: bob introduced cath (Catherine) in the channel", + <### [ EndsWith "updated to cath", + "#team: bob introduced cath (Catherine) in the channel", WithTime "#team> 1 [>>]", WithTime "#team> 2 [>>]", WithTime "#team> 3 [>>]", @@ -8927,7 +8958,9 @@ testChannelsSenderDeduplicateOwn ps = do WithTime "#team cath> 5 [>>]" ] eve - <### [ "#team: bob introduced cath (Catherine) in the channel", + <### [ EndsWith "updated to cath", + EndsWith "updated to dan", + "#team: bob introduced cath (Catherine) in the channel", "#team: bob introduced dan (Daniel) in the channel", WithTime "#team> 1 [>>]", WithTime "#team> 2 [>>]", @@ -8948,11 +8981,13 @@ testChannelLateJoinerReceivesProfile ps = (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob memberJoinChannel "team" [bob] [alice] shortLink fullLink cath memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + promoteChannelMember "team" alice bob cath [dan] - -- first forward: dan learns cath via prepended XGrpMemNew. + -- first forward: dan resolves cath (roster-known by id hash) on the prepended XGrpMemNew. cath #> "#team hi" bob <# "#team cath> hi" alice <# "#team cath> hi [>>]" + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hi [>>]" @@ -8988,12 +9023,23 @@ testChannel2RelaysDeduplicateProfile ps = memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink dan memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink eve + -- promote dan (observer default) so it can post; eve learns dan via the roster (id hash) + alice ##> "/mr #team dan member" + alice <## "#team: you changed the role of dan to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of dan from observer to member (signed)", + cath <## "#team: alice changed the role of dan from observer to member (signed)", + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"] + ] + -- first forward: both relays prepend XGrpMemNew(dan) for eve; -- second hits xGrpMemNew's "already created via another relay" branch. dan #> "#team hi" bob <# "#team dan> hi" cath <# "#team dan> hi" alice <# "#team dan> hi [>>]" + eve <### [EndsWith "updated to dan"] eve .<## " introduced dan (Daniel) in the channel" eve <# "#team dan> hi [>>]" @@ -9035,6 +9081,7 @@ testChannelLargeProfileFits ps = (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob memberJoinChannel "team" [bob] [alice] shortLink fullLink cath memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + promoteChannelMember "team" alice bob cath [dan] -- ~14000 chars: profile fits in a singleton batch AND packs -- inline with the forwarded body (exercises the in-body path). @@ -9045,6 +9092,7 @@ testChannelLargeProfileFits ps = cath #> "#team hi" bob <# "#team cath> hi" alice <# "#team cath> hi [>>]" + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hi [>>]" @@ -9058,6 +9106,8 @@ testChannelMultipleLargeProfiles ps = withNewTestChat ps "dan" danProfile $ \dan -> do withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + promoteChannelMember "team" alice bob dan [cath, eve] -- ~14500 chars each: one rides inline with the body, -- the other spills into a standalone overflow batch. @@ -9079,15 +9129,19 @@ testChannelMultipleLargeProfiles ps = WithTime "#team dan> from dan [>>]" ] cath - <### [ "#team: bob introduced dan (Daniel) in the channel", + <### [ EndsWith "updated to dan", + "#team: bob introduced dan (Daniel) in the channel", WithTime "#team dan> from dan [>>]" ] dan - <### [ "#team: bob introduced cath (Catherine) in the channel", + <### [ EndsWith "updated to cath", + "#team: bob introduced cath (Catherine) in the channel", WithTime "#team cath> from cath [>>]" ] eve - <### [ "#team: bob introduced dan (Daniel) in the channel", + <### [ EndsWith "updated to cath", + EndsWith "updated to dan", + "#team: bob introduced dan (Daniel) in the channel", "#team: bob introduced cath (Catherine) in the channel", WithTime "#team cath> from cath [>>]", WithTime "#team dan> from dan [>>]" @@ -9109,10 +9163,12 @@ testChannelProfileUpdateNoRePrepend ps = (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob memberJoinChannel "team" [bob] [alice] shortLink fullLink cath memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + promoteChannelMember "team" alice bob cath [dan] cath #> "#team hi" bob <# "#team cath> hi" alice <# "#team cath> hi [>>]" + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hi [>>]" @@ -9138,22 +9194,28 @@ testChannelMultiSendersIndependent ps = withNewTestChat ps "dan" danProfile $ \dan -> do withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + promoteChannelMember "team" alice bob dan [cath, eve] - -- cath posts: dan and eve learn cath via prepended XGrpMemNew + -- cath posts: dan and eve resolve cath on the prepended XGrpMemNew cath #> "#team from cath" bob <# "#team cath> from cath" alice <# "#team cath> from cath [>>]" + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> from cath [>>]" + eve <### [EndsWith "updated to cath"] eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> from cath [>>]" - -- dan posts: cath and eve learn dan independently of cath's vector + -- dan posts: cath and eve resolve dan independently of cath's vector dan #> "#team from dan" bob <# "#team dan> from dan" alice <# "#team dan> from dan [>>]" + cath <### [EndsWith "updated to dan"] cath <## "#team: bob introduced dan (Daniel) in the channel" cath <# "#team dan> from dan [>>]" + eve <### [EndsWith "updated to dan"] eve <## "#team: bob introduced dan (Daniel) in the channel" eve <# "#team dan> from dan [>>]" @@ -9174,6 +9236,17 @@ testChannels2RelaysDeliver ps = withNewTestChat ps "frank" frankProfile $ \frank -> do createChannel2Relays "team" alice bob cath dan eve frank + -- promote dan (observer default) so it can send; eve/frank learn dan via the roster + alice ##> "/mr #team dan member" + alice <## "#team: you changed the role of dan to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of dan from observer to member (signed)", + cath <## "#team: alice changed the role of dan from observer to member (signed)", + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + alice #> "#team hi" [bob, cath] *<# "#team> hi" [dan, eve, frank] *<# "#team> hi [>>]" @@ -9186,18 +9259,15 @@ testChannels2RelaysDeliver ps = cath <## " + 👍" alice <# "#team dan> > hi" alice <## " + 👍" + eve .<##. ("#team: unknown member ", " updated to dan") eve .<## " introduced dan (Daniel) in the channel" eve <# "#team dan> > hi" eve <## " + 👍" + frank .<##. ("#team: unknown member ", " updated to dan") frank .<## " introduced dan (Daniel) in the channel" frank <# "#team dan> > hi" frank <## " + 👍" - -- remove below if default role is changed to observer - dan #> "#team hey" - [bob, cath] *<# "#team dan> hey" - [alice, eve, frank] *<# "#team dan> hey [>>]" - testChannels2RelaysIncognito :: HasCallStack => TestParams -> IO () testChannels2RelaysIncognito ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do @@ -9211,6 +9281,17 @@ testChannels2RelaysIncognito ps = forM_ [eve, frank] $ \member -> memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink member + -- promote dan (observer default) so it can send; eve/frank learn dan via the roster + alice ##> ("/mr #team " <> danIncognito <> " member") + alice <## ("#team: you changed the role of " <> danIncognito <> " to member (signed)") + concurrentlyN_ + [ bob <## ("#team: alice changed the role of " <> danIncognito <> " from observer to member (signed)"), + cath <## ("#team: alice changed the role of " <> danIncognito <> " from observer to member (signed)"), + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + alice #> "#team hi" [bob, cath] *<# "#team> hi" dan ?<# "#team> hi [>>]" @@ -9224,18 +9305,15 @@ testChannels2RelaysIncognito ps = cath <## " + 👍" alice <# ("#team " <> danIncognito <> "> > hi") alice <## " + 👍" + eve .<##. ("#team: unknown member ", (" updated to " <> danIncognito)) eve .<## (" introduced " <> danIncognito <> " in the channel") eve <# ("#team " <> danIncognito <> "> > hi") eve <## " + 👍" + frank .<##. ("#team: unknown member ", (" updated to " <> danIncognito)) frank .<## (" introduced " <> danIncognito <> " in the channel") frank <# ("#team " <> danIncognito <> "> > hi") frank <## " + 👍" - -- remove below if default role is changed to observer - dan ?#> "#team hey" - [bob, cath] *<# ("#team " <> danIncognito <> "> hey") - [alice, eve, frank] *<# ("#team " <> danIncognito <> "> hey [>>]") - testChannelUpdateProfileSigned :: HasCallStack => TestParams -> IO () testChannelUpdateProfileSigned ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9293,7 +9371,7 @@ testChannelLinkAfterProfileUpdate ps = -- late subscriber joins via the same channel link after profile update threadDelay 100000 alice ##> "/show link #my_team" - (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRObserver False shortLink' `shouldBe` shortLink fullLink' `shouldBe` fullLink memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan @@ -9330,7 +9408,7 @@ testChannelLinkAfterWelcomeUpdate ps = -- re-fetch updated link, late subscriber joins threadDelay 100000 alice ##> "/show link #team" - (shortLink', fullLink') <- getGroupLinks alice "team" GRMember False + (shortLink', fullLink') <- getGroupLinks alice "team" GRObserver False shortLink' `shouldBe` shortLink fullLink' `shouldBe` fullLink memberJoinChannel "team" [bob] [alice] shortLink' fullLink' dan @@ -9367,7 +9445,7 @@ testChannelOwnerKeyAfterLinkUpdate ps = -- Late subscriber joins via the same channel link after profile update. alice ##> "/show link #my_team" - (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRObserver False shortLink' `shouldBe` shortLink fullLink' `shouldBe` fullLink memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan @@ -9434,15 +9512,20 @@ testChannelChangeRoleSigned ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- other members discover cath cath #> "#team hello from cath" bob <# "#team cath> hello from cath" concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do + eve <### [EndsWith "updated to cath"] eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -9463,21 +9546,21 @@ testChannelChangeRoleSigned ps = dan #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) eve #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) - -- change role of silent member (other members don't know about member) + -- change role of silent member; cath/eve don't know dan via xGrpMemRole, but the + -- subsequent roster apply emits the chat item with dan TOFU-created at the new role threadDelay 1000000 alice ##> "/mr #team dan admin" alice <## "#team: you changed the role of dan to admin (signed)" - bob <## "#team: alice changed the role of dan from member to admin (signed)" concurrentlyN_ - [ dan <## "#team: alice changed your role from member to admin (signed)", - cath <## "error: x.grp.mem.role with unknown member ID", - eve <## "error: x.grp.mem.role with unknown member ID" + [ bob <## "#team: alice changed the role of dan from observer to admin (signed)", + dan <## "#team: alice changed your role from observer to admin (signed)", + cath .<##. ("#team: alice changed the role of ", " from observer to admin (signed)"), + eve .<##. ("#team: alice changed the role of ", " from observer to admin (signed)") ] + -- cath/eve render dan by id hash (unknown to them, roster-TOFU); arrival verified above alice #$> ("/_get chat #1 count=1", chat, [(1, "changed role of dan to admin (signed)")]) bob #$> ("/_get chat #1 count=1", chat, [(0, "changed role of dan to admin (signed)")]) - cath #$> ("/_get chat #1 count=1", chat, [(0, "changed your role to admin (signed)")]) -- now new chat item dan #$> ("/_get chat #1 count=1", chat, [(0, "changed your role to admin (signed)")]) - eve #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) -- now new chat item testChannelBlockMemberSigned :: HasCallStack => TestParams -> IO () testChannelBlockMemberSigned ps = @@ -9488,6 +9571,9 @@ testChannelBlockMemberSigned ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- other members discover cath threadDelay 1000000 cath #> "#team hello from cath" @@ -9495,9 +9581,11 @@ testChannelBlockMemberSigned ps = concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do + eve <### [EndsWith "updated to cath"] eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -9543,6 +9631,224 @@ testChannelBlockMemberSigned ps = r2 `shouldStartWith` "blocked" r2 `shouldEndWith` "(signed)" +checkMemberRow :: HasCallStack => TestCC -> T.Text -> Maybe T.Text -> IO () +checkMemberRow cc name expectedRole = do + roles <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_role FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] + map (\(Only r) -> r) roles `shouldBe` maybeToList expectedRole + +testChannelModeratorActionViaRoster :: HasCallStack => TestParams -> IO () +testChannelModeratorActionViaRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + forM_ [cath, dan, eve] $ \member -> + memberJoinChannel "team" [bob] [alice] shortLink fullLink member + + -- promote dan (observer default) so it can post; cath and eve then discover dan + threadDelay 1000000 + promoteChannelMember "team" alice bob dan [cath, eve] + dan #> "#team hello from dan" + bob <# "#team dan> hello from dan" + concurrentlyN_ + [ alice <# "#team dan> hello from dan [>>]", + do + cath <### [EndsWith "updated to dan"] + cath <## "#team: bob introduced dan (Daniel) in the channel" + cath <# "#team dan> hello from dan [>>]", + do + eve <### [EndsWith "updated to dan"] + eve <## "#team: bob introduced dan (Daniel) in the channel" + eve <# "#team dan> hello from dan [>>]" + ] + + -- cath promoted observer -> moderator; dan/eve learn cath via the roster re-serve + -- (no name yet -> rendered by member id hash) + threadDelay 1000000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)", + dan <### [EndsWith "to moderator (signed)"], + eve <### [EndsWith "to moderator (signed)"] + ] + + -- cath (moderator) blocks dan; profile prepend carries cath's full profile to dan/eve + threadDelay 1000000 + cath ##> "/block for all #team dan" + cath <## "#team: you blocked dan (signed)" + bob <## "#team: cath blocked dan (signed)" + alice <## "#team: cath blocked dan (signed)" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <## "#team: cath blocked dan (signed)" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + + -- frank joins after the roster update; cached roster gives him cath as moderator. + -- both alice (owner) and cath (mod) receive XGrpMemNew(frank) via introduceInChannel. + -- the roster apply also emits the role-change chat item on frank's side (owner + -- profile may not be loaded yet, so the actor renders by memberId hash) + threadDelay 1000000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink frank + -- the late joiner learns the roster from the served snapshot (verified below); under the + -- no-broadcast model the apply finds no role change to surface, so no item here + threadDelay 1000000 -- the served roster arrives async + checkMemberRole frank "cath" "moderator" + where + checkMemberRole :: HasCallStack => TestCC -> T.Text -> T.Text -> IO () + checkMemberRole cc name expectedRole = do + roles <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_role FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] + map (\(Only r) -> r) roles `shouldBe` [expectedRole] + +testChannelRemovedModeratorRefreshesRoster :: HasCallStack => TestParams -> IO () +testChannelRemovedModeratorRefreshesRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + forM_ [cath, dan, eve] $ \member -> + memberJoinChannel "team" [bob] [alice] shortLink fullLink member + -- cath promoted observer -> moderator; dan/eve learn cath via the roster (id hash) + threadDelay 1000000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)", + dan <### [EndsWith "to moderator (signed)"], + eve <### [EndsWith "to moderator (signed)"] + ] + threadDelay 1000000 + alice ##> "/rm #team cath" + alice <## "#team: you removed cath from the group (signed)" + bob <## "#team: alice removed cath from the group (signed)" + cath <## "#team: alice removed you from the group (signed)" + cath <## "use /d #team to delete the group" + dan <### [EndsWith "from the group (signed)"] + eve <### [EndsWith "from the group (signed)"] + + -- frank joins after the removal; cached roster has dropped cath + threadDelay 1000000 + memberJoinChannel "team" [bob] [alice] shortLink fullLink frank + threadDelay 100000 + checkMemberRow frank "cath" Nothing + +testChannelRoleTransitionsUpdateRoster :: HasCallStack => TestParams -> IO () +testChannelRoleTransitionsUpdateRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + -- observer -> moderator + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + -- dan joins; cached roster has cath as moderator (learned from the served snapshot, + -- no separate role-change item under the no-broadcast model) + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink dan + threadDelay 1000000 -- the served roster arrives async; wait before reading the applied state + checkMemberRow dan "cath" (Just "moderator") + -- moderator -> admin: dan now knows cath, role event lands cleanly + threadDelay 100000 + alice ##> "/mr #team cath admin" + alice <## "#team: you changed the role of cath to admin (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from moderator to admin (signed)", + cath <## "#team: alice changed your role from moderator to admin (signed)", + dan <## "#team: alice changed the role of cath from moderator to admin (signed)" + ] + -- eve joins; cached roster has cath as admin (learned from the served snapshot) + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink eve + threadDelay 1000000 -- the served roster arrives async; wait before reading the applied state + checkMemberRow eve "cath" (Just "admin") + -- admin -> observer (crossing out of roster, since member is now in-roster): roster drops cath + threadDelay 100000 + alice ##> "/mr #team cath observer" + alice <## "#team: you changed the role of cath to observer (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from admin to observer (signed)", + cath <## "#team: alice changed your role from admin to observer (signed)", + dan <## "#team: alice changed the role of cath from admin to observer (signed)", + eve <## "#team: alice changed the role of cath from admin to observer (signed)" + ] + -- frank joins; cath isn't in the roster, so frank has no record of her + threadDelay 100000 + memberJoinChannel "team" [bob] [alice] shortLink fullLink frank + threadDelay 100000 + checkMemberRow frank "cath" Nothing + +testChannelRelayCannotDowngradeRosterMember :: HasCallStack => TestParams -> IO () +testChannelRelayCannotDowngradeRosterMember ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChatOpts ps (testOpts {coreOptions = testCoreOpts {logLevel = CLLWarning}}) "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink frank + -- promote cath; roster TOFU-creates cath on frank as moderator with the real key + threadDelay 1000000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)", + frank <### [EndsWith "to moderator (signed)"] + ] + threadDelay 100000 + realKey <- getMemberPubKey bob "cath" + -- malicious relay: corrupt bob's local record of cath so its XGrpMemNew dissemination + -- carries a downgraded role + no key + withCCTransaction bob $ \db -> + DB.execute + db + "UPDATE group_members SET member_role = ?, member_pub_key = NULL WHERE local_display_name = ?" + ("member" :: T.Text, "cath" :: T.Text) + -- cath posts; bob prepends XGrpMemNew(cath, member, NULL) to the delivery (frank not yet introduced) + threadDelay 100000 + cath #> "#team hello from cath" + bob <# "#team cath> hello from cath" + concurrentlyN_ + [ alice <# "#team cath> hello from cath [>>]", + do + frank <##. "warning: x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" + frank <### [EndsWith "updated to cath"] + frank <## "#team: bob introduced cath (Catherine) in the channel" + frank <# "#team cath> hello from cath [>>]" + ] + threadDelay 100000 + checkMemberRow frank "cath" (Just "moderator") + frankKey <- getMemberPubKey frank "cath" + frankKey `shouldBe` realKey + where + getMemberPubKey :: TestCC -> T.Text -> IO (Maybe ByteString) + getMemberPubKey cc name = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_pub_key FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only (Maybe ByteString)] + case rows of + [Only k] -> pure k + _ -> fail $ "expected one row for " <> T.unpack name + testChannelRemoveMemberSigned :: HasCallStack => TestParams -> IO () testChannelRemoveMemberSigned ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9552,15 +9858,20 @@ testChannelRemoveMemberSigned ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote eve to member (observer default) so it can post + promoteChannelMember "team" alice bob eve [cath, dan] + -- other members discover eve eve #> "#team hello from eve" bob <# "#team eve> hello from eve" concurrentlyN_ [ alice <# "#team eve> hello from eve [>>]", do + dan <### [EndsWith "updated to eve"] dan <## "#team: bob introduced eve (Eve) in the channel" dan <# "#team eve> hello from eve [>>]", do + cath <### [EndsWith "updated to eve"] cath <## "#team: bob introduced eve (Eve) in the channel" cath <# "#team eve> hello from eve [>>]" ] @@ -9733,6 +10044,9 @@ testChannelSubscriberLeave ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- other members discover cath threadDelay 1000000 cath #> "#team hello from cath" @@ -9740,9 +10054,11 @@ testChannelSubscriberLeave ps = concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do + eve <### [EndsWith "updated to cath"] eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -9968,6 +10284,9 @@ testChannelSubscriberProfileUpdate ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote dan to member early (observer default) so its role-change item precedes the messages + promoteChannelMember "team" alice bob dan [cath, eve] + -- enable support and create support chat for cath (but not dan) threadDelay 1000000 alice ##> "/set support #team on" @@ -9987,6 +10306,9 @@ testChannelSubscriberProfileUpdate ps = (dan "#team hello from cath" @@ -9994,9 +10316,11 @@ testChannelSubscriberProfileUpdate ps = concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do + eve <### [EndsWith "updated to cath"] eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -10026,9 +10350,8 @@ testChannelSubscriberProfileUpdate ps = cath #$> ("/_get chat #1 count=2", chat, [(1, "hello from cath"), (1, "hello from kate")]) -- verify profiles are updated correctly forM_ [alice, bob] $ \cc -> cc `hasContactProfiles` ["alice", "bob", "kate", "dan", "eve"] - cath `hasContactProfiles` ["alice", "bob", "kate"] dan `hasContactProfiles` ["alice", "bob", "kate", "dan"] - eve `hasContactProfiles` ["alice", "bob", "kate", "eve"] + -- cath/eve also know dan by id hash now (roster-learned before dan posts); not asserted -- previously silent subscriber updates profile -- dan has no support chat -> no profile update item created @@ -10040,9 +10363,11 @@ testChannelSubscriberProfileUpdate ps = concurrentlyN_ [ alice <# "#team dave> hello from dave [>>]", do + eve <### [EndsWith "updated to dave"] eve <## "#team: bob introduced dave in the channel" eve <# "#team dave> hello from dave [>>]", do + cath <### [EndsWith "updated to dave"] cath <## "#team: bob introduced dave in the channel" cath <# "#team dave> hello from dave [>>]" ] @@ -10128,6 +10453,250 @@ testChannelAddRelay ps = [bob, cath] *<# "#team> hello" [dan, eve] *<# "#team> hello [>>]" +testChannelAddRelayWithRoster :: HasCallStack => TestParams -> IO () +testChannelAddRelayWithRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "dan" danProfile $ \dan -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "eve" eveProfile $ \_eve -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + -- promote cath observer -> moderator: the roster is created (bob caches it) + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + threadDelay 100000 + + -- add dan as a 2nd relay; with a roster present it must cache the roster and ack + -- (XGrpRosterAck) before alice publishes it as joinable + dan ##> "/ad" + (danSLink, _cLink) <- getContactLinks dan True + alice ##> ("/relays name=dan " <> danSLink) + alice <## "ok" + alice ##> "/_add relays #1 2" + alice <## "#team: group relays:" + alice <## " - relay id 1: active" + alice <## " - relay id 2: invited" + concurrentlyN_ + [ do + alice <## "#team: group link relays updated, current relays:" + alice + <### [ " - relay id 1: active", + " - relay id 2: active" + ] + alice <## "group link:" + void $ getTermLine alice, + dan <## "#team: you joined the group as relay" + ] + + -- cath (an existing member) connects to the new relay and is attached to her roster + -- record, kept as moderator (the relay learned cath from the cached roster snapshot, so + -- it surfaces no role-change item for her) + concurrentlyN_ + [ do + cath <## "#team: joining the group (connecting to relay dan)..." + cath <## "#team: you joined the group (connected to relay dan)", + dan + <### [ EndsWith "accepting request to join group #team...", + EndsWith "is connected" + ] + ] + + threadDelay 100000 + -- the new relay holds the roster (cath is moderator) and learns her name when she connects + checkMemberRow dan "cath" (Just "moderator") + +testChannelRosterMultipartReassembly :: HasCallStack => TestParams -> IO () +testChannelRosterMultipartReassembly ps = + withNewTestChatCfgOpts ps cfg testOpts "alice" aliceProfile $ \alice -> + withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatCfgOpts ps cfg testOpts "cath" cathProfile $ \cath -> + withNewTestChatCfgOpts ps cfg testOpts "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink dan + -- dan reassembles the multi-chunk roster from the served snapshot (arrives async) + threadDelay 1000000 + checkMemberRow dan "cath" (Just "moderator") + where + cfg = testCfg {fileChunkSize = 30} + +testChannelRosterDigestMismatchRejected :: HasCallStack => TestParams -> IO () +testChannelRosterDigestMismatchRejected ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + threadDelay 100000 + -- corrupt the relay's stored blob (same length, different content) so its digest no + -- longer matches the signed header (DB-agnostic: read it, overwrite with zeroed bytes) + withCCTransaction bob $ \db -> do + rows <- DB.query_ db "SELECT roster_blob FROM groups WHERE roster_blob IS NOT NULL" :: IO [Only (Binary ByteString)] + forM_ rows $ \(Only (Binary blob)) -> + DB.execute db "UPDATE groups SET roster_blob = ? WHERE roster_blob IS NOT NULL" (Only (Binary (B.replicate (B.length blob) '\NUL'))) + -- frank joins; bob re-serves the valid header with the corrupted blob, frank rejects it + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink frank + threadDelay 1000000 + -- the rejected roster never elevates cath: the intro caps her to the channel default, so she + -- stays observer (not moderator), and the version must not advance to the corrupted roster's version 1 + checkMemberRow frank "cath" (Just "observer") + checkRosterNotApplied frank + where + -- the version is the second guarantee (the role is asserted above): frank holds exactly the team + -- group with no roster applied, so roster_version is NULL - it never advanced to the corrupted version 1 + checkRosterNotApplied :: HasCallStack => TestCC -> IO () + checkRosterNotApplied cc = do + vs <- withCCTransaction cc $ \db -> + DB.query_ db "SELECT roster_version FROM groups" :: IO [Only (Maybe Int64)] + map (\(Only v) -> v) vs `shouldBe` [Nothing] + +testChannelPromotedMemberCanPost :: HasCallStack => TestParams -> IO () +testChannelPromotedMemberCanPost ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + -- promote cath to member: cath enters the owner-signed roster (dan learns cath by id hash) + promoteChannelMember "team" alice bob cath [dan] + -- the promoted member can now post; dan resolves cath on the first forward + cath #> "#team hi from cath" + bob <# "#team cath> hi from cath" + alice <# "#team cath> hi from cath [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> hi from cath [>>]" + checkMemberRow dan "cath" (Just "member") + +testChannelObserverCannotPost :: HasCallStack => TestParams -> IO () +testChannelObserverCannotPost ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + -- cath is an observer (default): its own post is rejected locally and never reaches the relay + cath ##> "#team observer attempt" + cath <## "#team: you don't have permission to send messages" + -- promote cath to member; the post is now accepted and delivered, dan resolves cath + promoteChannelMember "team" alice bob cath [dan] + cath #> "#team member post" + bob <# "#team cath> member post" + alice <# "#team cath> member post [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> member post [>>]" + +testChannelPromotedMemberRejoinViaRelay :: HasCallStack => TestParams -> IO () +testChannelPromotedMemberRejoinViaRelay ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "dan" danProfile $ \dan -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + -- promote cath to member: cath enters the owner-signed roster with her pinned key + threadDelay 100000 + promoteChannelMember "team" alice bob cath [] + threadDelay 100000 + -- add dan as a 2nd relay; it caches the roster (incl. member cath) before joinable + dan ##> "/ad" + (danSLink, _cLink) <- getContactLinks dan True + alice ##> ("/relays name=dan " <> danSLink) + alice <## "ok" + alice ##> "/_add relays #1 2" + alice <## "#team: group relays:" + alice <## " - relay id 1: active" + alice <## " - relay id 2: invited" + concurrentlyN_ + [ do + alice <## "#team: group link relays updated, current relays:" + alice + <### [ " - relay id 1: active", + " - relay id 2: active" + ] + alice <## "group link:" + void $ getTermLine alice, + dan <## "#team: you joined the group as relay" + ] + -- cath (a promoted member) connects to the new relay; the widened join gate + -- (verifyKey over the roster-pinned key) accepts her and keeps her as member + concurrentlyN_ + [ do + cath <## "#team: joining the group (connecting to relay dan)..." + cath <## "#team: you joined the group (connected to relay dan)", + dan + <### [ EndsWith "accepting request to join group #team...", + EndsWith "is connected" + ] + ] + threadDelay 100000 + checkMemberRow dan "cath" (Just "member") + +testChannelRosterMultiRelayMultipart :: HasCallStack => TestParams -> IO () +testChannelRosterMultiRelayMultipart ps = + withNewTestChatCfgOpts ps cfg testOpts "alice" aliceProfile $ \alice -> + withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatCfgOpts ps cfg relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChatCfgOpts ps cfg testOpts "dan" danProfile $ \dan -> + withNewTestChatCfgOpts ps cfg testOpts "eve" eveProfile $ \eve -> + withNewTestChatCfgOpts ps cfg testOpts "frank" frankProfile $ \frank -> do + createChannel2Relays "team" alice bob cath dan eve frank + + -- promote eve to moderator: the owner-signed roster broadcasts through BOTH relays to dan and + -- frank (each connected to both). At fileChunkSize=30 the blob spans multiple chunks, so each + -- member receives two interleaved multi-chunk streams (one per relay) for the same roster. + threadDelay 1000000 + alice ##> "/mr #team eve moderator" + alice <## "#team: you changed the role of eve to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of eve from observer to moderator (signed)", + cath <## "#team: alice changed the role of eve from observer to moderator (signed)", + eve <## "#team: alice changed your role from observer to moderator (signed)", + dan <### [EndsWith "to moderator (signed)"], + frank <### [EndsWith "to moderator (signed)"] + ] + threadDelay 1000000 -- let both relays' interleaved multipart streams settle + + -- per-source transfers keep the streams independent, so each member reassembles the blob and pins + -- eve as the single moderator WITH her owner-attested key (role + key both come from the blob) + checkOneModeratorWithKey dan + checkOneModeratorWithKey frank + where + cfg = testCfg {fileChunkSize = 30} + checkOneModeratorWithKey cc = do + rows <- withCCTransaction cc $ \db -> + DB.query_ db "SELECT member_pub_key FROM group_members WHERE member_role = 'moderator'" :: IO [Only (Maybe ByteString)] + map (\(Only k) -> isJust k) rows `shouldBe` [True] + testChannelRemoveRelay :: HasCallStack => TestParams -> IO () testChannelRemoveRelay ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -10708,42 +11277,48 @@ testChannelMessageFile ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - + -- the roster arrives as a file before this one; Postgres assigns it a new id and does not + -- reuse it on delete (SQLite does), so the received message file is id 2 here, 1 on SQLite. +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as channel message alice #> "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- all members receive the file concurrently src <- B.readFile "./tests/fixtures/test.jpg" concurrentlyN_ - [ receiveFile bob "bob" src, - receiveFile cath "cath" src, - receiveFile dan "dan" src, - receiveFile eve "eve" src + [ receiveFile bob "bob" rcvFileId src, + receiveFile cath "cath" rcvFileId src, + receiveFile dan "dan" rcvFileId src, + receiveFile eve "eve" rcvFileId src ] where - receiveFile cc name src = do + receiveFile cc name fileId src = do let path = "./tests/tmp/test_" <> name <> ".jpg" - cc ##> ("/fr 1 " <> path) + cc ##> ("/fr " <> show fileId <> " " <> path) cc - <### [ ConsoleString ("saving file 1 from #team to " <> path), - "started receiving file 1 (test.jpg) from #team" + <### [ ConsoleString ("saving file " <> show fileId <> " from #team to " <> path), + ConsoleString ("started receiving file " <> show fileId <> " (test.jpg) from #team") ] - cc <## "completed receiving file 1 (test.jpg) from #team" + cc <## ("completed receiving file " <> show fileId <> " (test.jpg) from #team") B.readFile path >>= (`shouldBe` src) testChannelMessageFileCancel :: HasCallStack => TestParams -> IO () @@ -10754,33 +11329,37 @@ testChannelMessageFileCancel ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as channel message alice #> "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- owner cancels file alice ##> "/fc 1" alice <## "cancelled sending file 1 (test.jpg) to bob" - bob <## "team cancelled sending file 1 (test.jpg)" + bob <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)") concurrentlyN_ - [ cath <## "team cancelled sending file 1 (test.jpg)", - dan <## "team cancelled sending file 1 (test.jpg)", - eve <## "team cancelled sending file 1 (test.jpg)" + [ cath <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + dan <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + eve <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)") ] testChannelMessageQuote :: HasCallStack => TestParams -> IO () @@ -10797,6 +11376,9 @@ testChannelMessageQuote ps = bob <# "#team> hello from channel" [cath, dan, eve] *<# "#team> hello from channel [>>]" + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- member quotes channel message cath `send` "> #team (hello from) replying to channel" cath <# "#team > hello from channel" @@ -10808,10 +11390,12 @@ testChannelMessageQuote ps = alice <# "#team cath> > hello from channel [>>]" alice <## " replying to channel [>>]", do + dan <### [EndsWith "updated to cath"] dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> > hello from channel [>>]" dan <## " replying to channel [>>]", do + eve <### [EndsWith "updated to cath"] eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> > hello from channel [>>]" eve <## " replying to channel [>>]" @@ -10925,43 +11509,47 @@ testChannelOwnerFileTransferAsMember ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as member (not as channel) alice ##> "/_send #1(as_group=off) json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}]" alice <# "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- all members receive the file src <- B.readFile "./tests/fixtures/test.jpg" concurrentlyN_ - [ receiveFile bob "bob" src, - receiveFile cath "cath" src, - receiveFile dan "dan" src, - receiveFile eve "eve" src + [ receiveFile bob "bob" rcvFileId src, + receiveFile cath "cath" rcvFileId src, + receiveFile dan "dan" rcvFileId src, + receiveFile eve "eve" rcvFileId src ] where - receiveFile cc name src = do + receiveFile cc name fileId src = do let path = "./tests/tmp/test_" <> name <> ".jpg" - cc ##> ("/fr 1 " <> path) + cc ##> ("/fr " <> show fileId <> " " <> path) cc - <### [ ConsoleString ("saving file 1 from alice to " <> path), - "started receiving file 1 (test.jpg) from alice" + <### [ ConsoleString ("saving file " <> show fileId <> " from alice to " <> path), + ConsoleString ("started receiving file " <> show fileId <> " (test.jpg) from alice") ] - cc <## "completed receiving file 1 (test.jpg) from alice" + cc <## ("completed receiving file " <> show fileId <> " (test.jpg) from alice") B.readFile path >>= (`shouldBe` src) testChannelOwnerFileCancelAsMember :: HasCallStack => TestParams -> IO () @@ -10972,34 +11560,38 @@ testChannelOwnerFileCancelAsMember ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as member (not as channel) alice ##> "/_send #1(as_group=off) json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}]" alice <# "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- owner cancels file alice ##> "/fc 1" alice <## "cancelled sending file 1 (test.jpg) to bob" - bob <## "alice cancelled sending file 1 (test.jpg)" + bob <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)") concurrentlyN_ - [ cath <## "alice cancelled sending file 1 (test.jpg)", - dan <## "alice cancelled sending file 1 (test.jpg)", - eve <## "alice cancelled sending file 1 (test.jpg)" + [ cath <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + dan <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + eve <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)") ] testChannelReactionAttribution :: HasCallStack => TestParams -> IO () @@ -11163,14 +11755,19 @@ testChannelMemberMessageUpdate ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- member sends a message cath #> "#team hello" bob <# "#team cath> hello" concurrentlyN_ [ alice <# "#team cath> hello [>>]", - do dan <## "#team: bob introduced cath (Catherine) in the channel" + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello [>>]", - do eve <## "#team: bob introduced cath (Catherine) in the channel" + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello [>>]" ] @@ -11194,14 +11791,19 @@ testChannelMemberMessageDelete ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- member sends a message cath #> "#team hello" bob <# "#team cath> hello" concurrentlyN_ [ alice <# "#team cath> hello [>>]", - do dan <## "#team: bob introduced cath (Catherine) in the channel" + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello [>>]", - do eve <## "#team: bob introduced cath (Catherine) in the channel" + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello [>>]" ] diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index d76c86495f..2592420d01 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -155,7 +155,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-18\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-19\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" @@ -266,13 +266,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-18\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-18\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" @@ -287,7 +287,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-18\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" @@ -300,7 +300,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XGrpMemConAll (MemberId "\1\2\3\4") it "x.grp.mem.del" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" - #==# XGrpMemDel (MemberId "\1\2\3\4") False + #==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing it "x.grp.leave" $ "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}" ==# XGrpLeave From 117ed75d4012e0695ca7acf060c24627b5dcd02a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:53:05 +0000 Subject: [PATCH 16/18] ui: allow to change member role in channel (#7096) --- apps/ios/Shared/Views/Chat/ChatView.swift | 2 +- .../Chat/Group/AddGroupMembersView.swift | 2 +- .../Views/Chat/Group/ChannelMembersView.swift | 2 +- .../Views/Chat/Group/GroupChatInfoView.swift | 2 +- .../Views/Chat/Group/GroupLinkView.swift | 2 +- .../Chat/Group/GroupMemberInfoView.swift | 18 ++++++----- .../Views/Chat/Group/MemberSupportView.swift | 2 +- apps/ios/SimpleXChat/ChatTypes.swift | 28 ++++++++++++----- .../chat/simplex/common/model/ChatModel.kt | 23 +++++++++----- .../simplex/common/views/chat/ChatView.kt | 2 +- .../views/chat/group/AddGroupMembersView.kt | 2 +- .../views/chat/group/ChannelMembersView.kt | 10 +++--- .../views/chat/group/GroupChatInfoView.kt | 8 ++--- .../common/views/chat/group/GroupLinkView.kt | 2 +- .../views/chat/group/GroupMemberInfoView.kt | 31 +++++++++---------- .../views/chat/group/MemberSupportView.kt | 6 ++-- .../commonMain/resources/MR/base/strings.xml | 5 ++- 17 files changed, 85 insertions(+), 62 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 49643745b0..ded0019692 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -1999,7 +1999,7 @@ struct ChatView: View { let (name, role) = if ci.meta.showGroupAsSender { (groupInfo.chatViewName, NSLocalizedString("group", comment: "shown on group welcome message")) } else { - (member.chatViewName, member.memberRole.text) + (member.chatViewName, member.memberRole.text(isChannel: groupInfo.isChannel)) } Group { if #available(iOS 16.0, *) { diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index b59fd51fe8..336b4adfd1 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -183,7 +183,7 @@ struct AddGroupMembersViewCommon: View { private func rolePicker() -> some View { Picker("New member role", selection: $selectedRole) { ForEach(GroupMemberRole.supportedRoles.filter({ $0 <= groupInfo.membership.memberRole })) { role in - Text(role.text) + Text(role.text(isChannel: groupInfo.isChannel)) } } .frame(height: 36) diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift index 50144e2bc5..44fc302aff 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift @@ -66,7 +66,7 @@ struct ChannelMembersView: View { } Spacer() if showRole { - Text(member.memberRole.text) + Text(member.memberRole.text(isChannel: groupInfo.isChannel)) .foregroundColor(theme.colors.secondary) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index b62939fd36..5563d79e61 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -581,7 +581,7 @@ struct GroupChatInfoView: View { } else { let role = member.memberRole if [.owner, .admin, .moderator, .observer].contains(role) { - Text(member.memberRole.text) + Text(member.memberRole.text(isChannel: groupInfo.isChannel)) .foregroundColor(theme.colors.secondary) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 22253c4808..43d23878f5 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -84,7 +84,7 @@ struct GroupLinkView: View { if !isChannel { Picker("Initial role", selection: $groupLinkMemberRole) { ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in - Text(role.text) + Text(role.text(isChannel: isChannel)) } } .frame(height: 36) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 2f37e3d822..c87f97089c 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -178,15 +178,15 @@ struct GroupMemberInfoView: View { let label: LocalizedStringKey = groupInfo.useRelays ? "Channel" : groupInfo.businessChat == nil ? "Group" : "Chat" infoRow(label, groupInfo.displayName) - if !groupInfo.useRelays, let roles = member.canChangeRoleTo(groupInfo: groupInfo) { + if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { Picker("Change role", selection: $newRole) { ForEach(roles) { role in - Text(role.text) + Text(role.text(isChannel: groupInfo.isChannel)) } } .frame(height: 36) } else { - infoRow("Role", member.memberRole.text) + infoRow("Role", member.memberRole.text(isChannel: groupInfo.isChannel)) } if let link = member.relayLink { infoRow("Relay link", String.localizedStringWithFormat(NSLocalizedString("via %@", comment: "relay hostname"), hostFromRelayLink(link))) @@ -727,15 +727,17 @@ struct GroupMemberInfoView: View { private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert { Alert( - title: Text("Change member role?"), + title: Text("Change role?"), message: ( mem.memberCurrent ? ( - groupInfo.businessChat == nil - ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") - : Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.") + groupInfo.isChannel + ? Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All subscribers will be notified.") + : groupInfo.businessChat == nil + ? Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All group members will be notified.") + : Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All chat members will be notified.") ) - : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.") + : Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". The member will receive a new invitation.") ), primaryButton: .default(Text("Change")) { Task { diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift index 0263a39a90..da9d56a699 100644 --- a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -205,7 +205,7 @@ struct MemberSupportView: View { } else if member.memberPending { return member.memberStatus.text } else { - return LocalizedStringKey(member.memberRole.text) + return LocalizedStringKey(member.memberRole.text(isChannel: groupInfo.isChannel)) } } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index d7478e5c3b..8b186f168d 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2981,8 +2981,16 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { if memberRole == .relay || !canBeRemoved(groupInfo: groupInfo) || memberStatus == .memRemoved || memberStatus == .memLeft || memberPending { return nil } + if groupInfo.useRelays && !groupInfo.isOwner { return nil } let userRole = groupInfo.membership.memberRole - return GroupMemberRole.supportedRoles.filter { $0 <= userRole } + if groupInfo.useRelays { + // TODO [relays]: for now owners can only set observer/member in channels. + // Restore the full Owner-excluded picker when moderator/admin promotion is supported: + // return GroupMemberRole.supportedRoles.filter { $0 <= userRole && $0 != .owner } + return [.observer, .member] + } else { + return GroupMemberRole.supportedRoles.filter { $0 <= userRole } + } } public func canBlockForAll(groupInfo: GroupInfo) -> Bool { @@ -3070,12 +3078,16 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod public static var supportedRoles: [GroupMemberRole] = [.observer, .member, .moderator, .admin, .owner] - public var text: String { + public func text(isChannel: Bool) -> String { switch self { case .relay: return NSLocalizedString("relay", comment: "member role") - case .observer: return NSLocalizedString("observer", comment: "member role") + case .observer: return isChannel + ? NSLocalizedString("subscriber", comment: "member role") + : NSLocalizedString("observer", comment: "member role") case .author: return NSLocalizedString("author", comment: "member role") - case .member: return NSLocalizedString("member", comment: "member role") + case .member: return isChannel + ? NSLocalizedString("contributor", comment: "member role") + : NSLocalizedString("member", comment: "member role") case .moderator: return NSLocalizedString("moderator", comment: "member role") case .admin: return NSLocalizedString("admin", comment: "member role") case .owner: return NSLocalizedString("owner", comment: "member role") @@ -5596,7 +5608,7 @@ public enum RcvGroupEvent: Decodable, Hashable { case .userAccepted: return NSLocalizedString("accepted you", comment: "rcv group event chat item") case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberRole(_, profile, role): - return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text) + return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text(isChannel: isChannel)) case let .memberBlocked(_, profile, blocked): if blocked { return String.localizedStringWithFormat(NSLocalizedString("blocked %@", comment: "rcv group event chat item"), profile.profileViewName) @@ -5604,7 +5616,7 @@ public enum RcvGroupEvent: Decodable, Hashable { return String.localizedStringWithFormat(NSLocalizedString("unblocked %@", comment: "rcv group event chat item"), profile.profileViewName) } case let .userRole(role): - return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text) + return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text(isChannel: isChannel)) case let .memberDeleted(_, profile): return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName) case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item") @@ -5650,9 +5662,9 @@ public enum SndGroupEvent: Decodable, Hashable { func text(isChannel: Bool) -> String { switch self { case let .memberRole(_, profile, role): - return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text) + return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text(isChannel: isChannel)) case let .userRole(role): - return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text) + return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text(isChannel: isChannel)) case let .memberBlocked(_, profile, blocked): if blocked { return String.localizedStringWithFormat(NSLocalizedString("you blocked %@", comment: "snd group event chat item"), profile.profileViewName) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index ecfb58fdb0..c5dd81fec7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2617,8 +2617,15 @@ data class GroupMember ( fun canChangeRoleTo(groupInfo: GroupInfo): List? = if (memberRole == GroupMemberRole.Relay || !canBeRemoved(groupInfo) || memberStatus == GroupMemberStatus.MemRemoved || memberStatus == GroupMemberStatus.MemLeft || memberPending) null + else if (groupInfo.useRelays && !groupInfo.isOwner) null else groupInfo.membership.memberRole.let { userRole -> - GroupMemberRole.selectableRoles.filter { it <= userRole } + if (groupInfo.useRelays) + // TODO [relays]: for now owners can only set observer/member in channels. + // Restore the full Owner-excluded picker when moderator/admin promotion is supported: + // GroupMemberRole.selectableRoles.filter { it <= userRole && it != GroupMemberRole.Owner } + listOf(GroupMemberRole.Observer, GroupMemberRole.Member) + else + GroupMemberRole.selectableRoles.filter { it <= userRole } } fun canBlockForAll(groupInfo: GroupInfo): Boolean { @@ -2696,11 +2703,11 @@ enum class GroupMemberRole(val memberRole: String) { val selectableRoles: List = listOf(Observer, Member, Moderator, Admin, Owner) } - val text: String get() = when (this) { + fun text(isChannel: Boolean): String = when (this) { Relay -> generalGetString(MR.strings.group_member_role_relay) - Observer -> generalGetString(MR.strings.group_member_role_observer) + Observer -> generalGetString(if (isChannel) MR.strings.group_member_role_observer_channel else MR.strings.group_member_role_observer) Author -> generalGetString(MR.strings.group_member_role_author) - Member -> generalGetString(MR.strings.group_member_role_member) + Member -> generalGetString(if (isChannel) MR.strings.group_member_role_member_channel else MR.strings.group_member_role_member) Moderator -> generalGetString(MR.strings.group_member_role_moderator) Admin -> generalGetString(MR.strings.group_member_role_admin) Owner -> generalGetString(MR.strings.group_member_role_owner) @@ -5094,13 +5101,13 @@ sealed class RcvGroupEvent() { is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName) is UserAccepted -> generalGetString(MR.strings.rcv_group_event_user_accepted) is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left) - is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text) + is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text(isChannel = isChannel)) is MemberBlocked -> if (blocked) { String.format(generalGetString(MR.strings.rcv_group_event_member_blocked), profile.profileViewName) } else { String.format(generalGetString(MR.strings.rcv_group_event_member_unblocked), profile.profileViewName) } - is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text) + is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text(isChannel = isChannel)) is MemberDeleted -> String.format(generalGetString(MR.strings.rcv_group_event_member_deleted), profile.profileViewName) is UserDeleted -> generalGetString(MR.strings.rcv_group_event_user_deleted) is GroupDeleted -> generalGetString(if (isChannel) MR.strings.rcv_channel_event_channel_deleted else MR.strings.rcv_group_event_group_deleted) @@ -5139,8 +5146,8 @@ sealed class SndGroupEvent() { val text: String get() = text(isChannel = false) fun text(isChannel: Boolean): String = when (this) { - is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text) - is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text) + is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text(isChannel = isChannel)) + is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text(isChannel = isChannel)) is MemberBlocked -> if (blocked) { String.format(generalGetString(MR.strings.snd_group_event_member_blocked), profile.profileViewName) } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index a020f8c1fe..6f7c746691 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -2035,7 +2035,7 @@ fun BoxScope.ChatItemsList( val tailRendered = style is ShapeStyle.Bubble && style.tailVisible Text( - member.memberRole.text, + member.memberRole.text(isChannel = chatInfo.isChannel), Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp), fontSize = 13.5.sp, fontWeight = FontWeight.Medium, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 4717321f63..7223f69f98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -228,7 +228,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState Divider() @@ -55,7 +55,7 @@ fun ChannelMembersView( minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING) ) { - ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Owner) + ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Owner, isChannel = groupInfo.isChannel) } } } @@ -71,7 +71,7 @@ fun ChannelMembersView( minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING) ) { - ChannelMemberRow(member, user = false, showRole = false) + ChannelMemberRow(member, user = false, showRole = false, isChannel = groupInfo.isChannel) } } } @@ -81,7 +81,7 @@ fun ChannelMembersView( } @Composable -private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boolean) { +private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boolean, isChannel: Boolean) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -112,7 +112,7 @@ private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boole } if (showRole) { Text( - member.memberRole.text, + member.memberRole.text(isChannel = isChannel), color = MaterialTheme.colors.secondary ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index b2c25bf06c..2b1c7bcd09 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -731,7 +731,7 @@ fun ModalData.GroupChatInfoLayout( } } SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - MemberRow(groupInfo.membership, user = true) + MemberRow(groupInfo.membership, user = true, isChannel = groupInfo.isChannel) } } } @@ -765,7 +765,7 @@ fun ModalData.GroupChatInfoLayout( val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp) DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu) Box(Modifier.padding(start = selectionOffset)) { - MemberRow(member) + MemberRow(member, isChannel = groupInfo.isChannel) } } } @@ -1057,7 +1057,7 @@ private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialThem } @Composable -fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = true, showlocalAliasAndFullName: Boolean = false, selected: Boolean = false) { +fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = true, showlocalAliasAndFullName: Boolean = false, selected: Boolean = false, isChannel: Boolean = false) { @Composable fun MemberInfo() { if (member.blocked) { @@ -1065,7 +1065,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr } else { val role = member.memberRole if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Moderator, GroupMemberRole.Observer)) { - Text(role.text, color = MaterialTheme.colors.secondary) + Text(role.text(isChannel = isChannel), color = MaterialTheme.colors.secondary) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index b68f0efaf5..f6f19e9d72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -308,7 +308,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState Unit) { private fun RoleSelectionRow( roles: List, selectedRole: MutableState, - onSelected: (GroupMemberRole) -> Unit + onSelected: (GroupMemberRole) -> Unit, + isChannel: Boolean ) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - val values = remember { roles.map { it to it.text } } + val values = remember { roles.map { it to it.text(isChannel = isChannel) } } ExposedDropDownSettingRow( generalGetString(MR.strings.change_role), values, @@ -956,12 +953,14 @@ fun updateMemberRoleDialog( AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.change_member_role_question), text = if (memberCurrent) { - if (groupInfo.businessChat == null) - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) + if (groupInfo.isChannel) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_channel), newRole.text(isChannel = groupInfo.isChannel)) + else if (groupInfo.businessChat == null) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text(isChannel = groupInfo.isChannel)) else - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text(isChannel = groupInfo.isChannel)) } else - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text), + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text(isChannel = groupInfo.isChannel)), confirmText = generalGetString(MR.strings.change_verb), onDismiss = onDismiss, onConfirm = onConfirm, @@ -977,9 +976,9 @@ fun updateMembersRoleDialog( AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.change_member_role_question), text = if (groupInfo.businessChat == null) - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text(isChannel = groupInfo.isChannel)) else - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text), + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text(isChannel = groupInfo.isChannel)), confirmText = generalGetString(MR.strings.change_verb), onConfirm = onConfirm, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt index 7ca277df94..b685d20fb9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -150,7 +150,7 @@ private fun ModalData.MemberSupportViewLayout( ) { Box(contentAlignment = Alignment.CenterStart) { DropDownMenuForSupportChat(chat.remoteHostId, member, groupInfo, showMenu) - SupportChatRow(member) + SupportChatRow(member, isChannel = groupInfo.isChannel) } } } @@ -163,7 +163,7 @@ private fun ModalData.MemberSupportViewLayout( } @Composable -fun SupportChatRow(member: GroupMember) { +fun SupportChatRow(member: GroupMember, isChannel: Boolean) { fun memberStatus(): String { return if (member.activeConn?.connStatus is ConnStatus.Failed) { generalGetString(MR.strings.member_info_member_failed) @@ -174,7 +174,7 @@ fun SupportChatRow(member: GroupMember) { } else if (member.memberPending) { member.memberStatus.text } else { - member.memberRole.text + member.memberRole.text(isChannel = isChannel) } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 4ea07d4608..71c0bf98a4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1850,8 +1850,10 @@ observer + subscriber author member + contributor moderator admin owner @@ -2043,9 +2045,10 @@ Change role Change Switch - Change group role? + Change role? The role will be changed to "%s". Everyone in the group will be notified. The role will be changed to "%s". Everyone in the chat will be notified. + The role will be changed to "%s". Everyone in the channel will be notified. The role will be changed to "%s". The member will receive a new invitation. Connect directly? Сonnection request will be sent to this group member. From 59fce95d3cd08897b4ef742447b785cf2e56c7ce Mon Sep 17 00:00:00 2001 From: SimpleX Chat Date: Mon, 22 Jun 2026 11:15:55 +0000 Subject: [PATCH 17/18] 6.5.6: android 358, desktop 148, ios 337 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 56 +++++++++---------- apps/multiplatform/gradle.properties | 8 +-- packages/simplex-chat-nodejs/package.json | 2 +- .../simplex-chat-nodejs/src/download-libs.js | 2 +- .../src/simplex_chat/_version.py | 4 +- .../flatpak/chat.simplex.simplex.metainfo.xml | 22 ++++++++ 6 files changed, 58 insertions(+), 36 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 0739e9522d..0e585fbe15 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -184,8 +184,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -563,8 +563,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -733,8 +733,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -820,8 +820,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.5.0-57WEoB2fiWaFgCB1XksYmP.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a */, ); path = Libraries; sourceTree = ""; @@ -2077,7 +2077,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2102,7 +2102,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2127,7 +2127,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2152,7 +2152,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2169,11 +2169,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2189,11 +2189,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2214,7 +2214,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2229,7 +2229,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2251,7 +2251,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2266,7 +2266,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2288,7 +2288,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2314,7 +2314,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2339,7 +2339,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2366,7 +2366,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2393,7 +2393,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2408,7 +2408,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2427,7 +2427,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 335; + CURRENT_PROJECT_VERSION = 337; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2442,7 +2442,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.5.5; + MARKETING_VERSION = 6.5.6; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 61c744bf86..d0cc299697 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5.5 -android.version_code=355 +android.version_name=6.5.6 +android.version_code=358 android.bundle=false -desktop.version_name=6.5.5 -desktop.version_code=146 +desktop.version_name=6.5.6 +desktop.version_code=148 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 40ef106103..e5360df2b4 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "simplex-chat", - "version": "6.5.5", + "version": "6.5.6", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js index 4fad0f45a9..8fbed9efe0 100644 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -4,7 +4,7 @@ const path = require('path'); const extract = require('extract-zip'); const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; -const RELEASE_TAG = 'v6.5.5'; +const RELEASE_TAG = 'v6.5.6'; const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase(); if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') { diff --git a/packages/simplex-chat-python/src/simplex_chat/_version.py b/packages/simplex-chat-python/src/simplex_chat/_version.py index 77acd0e8b3..0b25146728 100644 --- a/packages/simplex-chat-python/src/simplex_chat/_version.py +++ b/packages/simplex-chat-python/src/simplex_chat/_version.py @@ -5,5 +5,5 @@ Bump both together for normal releases. For wrapper-only fixes use a PEP 440 post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged. """ -__version__ = "6.5.5" # PEP 440 — read by hatchling for wheel metadata -LIBS_VERSION = "6.5.5" # simplex-chat-libs release tag (no 'v' prefix) +__version__ = "6.5.6" # PEP 440 — read by hatchling for wheel metadata +LIBS_VERSION = "6.5.6" # simplex-chat-libs release tag (no 'v' prefix) diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index 0d93315435..b527720e5b 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,28 @@ + + https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html + +

New in v6.5.6:

+

Public channels - speak freely!

+
    +
  • Reliability: many relays per channel.
  • +
  • Ownership: you can run your own relays.
  • +
  • Security: owners hold channel keys.
  • +
  • Privacy: for owners and subscribers.
  • +
+

Easier to invite your friends: we made connecting simpler for new users.

+

Safe web links:

+
    +
  • opt-in to send link previews.
  • +
  • use SOCKS proxy for previews (if enabled).
  • +
  • prevent hyperlink phishing.
  • +
  • remove link tracking.
  • +
+

Non-profit governance: to make SimpleX Network last.

+
+
https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html From 514bfc8519c350bbcbcc4c94d6c6a457881cefb3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 22 Jun 2026 16:48:01 +0100 Subject: [PATCH 18/18] core: 7.0.0.4 --- .../xcshareddata/swiftpm/Package.resolved | 66 ------------------- simplex-chat.cabal | 2 +- 2 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 978e1d9630..0000000000 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,66 +0,0 @@ -{ - "originHash" : "60aeecb7917535a5e44ade0dbb5411ab112a959283e565a04c212c8af4e7dee9", - "pins" : [ - { - "identity" : "codescanner", - "kind" : "remoteSourceControl", - "location" : "https://github.com/twostraws/CodeScanner", - "state" : { - "revision" : "34da57fb63b47add20de8a85da58191523ccce57", - "version" : "2.5.0" - } - }, - { - "identity" : "elegant-emoji-picker", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Finalet/Elegant-Emoji-Picker", - "state" : { - "branch" : "main", - "revision" : "71d2d46092b4d550cc593614efc06438f845f6e6" - } - }, - { - "identity" : "ink", - "kind" : "remoteSourceControl", - "location" : "https://github.com/johnsundell/ink", - "state" : { - "revision" : "bcc9f219900a62c4210e6db726035d7f03ae757b", - "version" : "0.6.0" - } - }, - { - "identity" : "lzstring-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Ibrahimhass/lzstring-swift", - "state" : { - "revision" : "7f62f21de5b18582a950e1753b775cc614722407" - } - }, - { - "identity" : "swiftygif", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kirualex/SwiftyGif", - "state" : { - "revision" : "5e8619335d394901379c9add5c4c1c2f420b3800" - } - }, - { - "identity" : "webrtc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/simplex-chat/WebRTC.git", - "state" : { - "revision" : "34bedc50f9c58dccf4967ea59c7e6a47d620803b" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams", - "state" : { - "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", - "version" : "5.1.2" - } - } - ], - "version" : 3 -} diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f1effc2e6a..64ec8dcf13 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 7.0.0.3 +version: 7.0.0.4 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat