From 340e1c0219a9c5101110a6d7e597dc1a2a549706 Mon Sep 17 00:00:00 2001 From: XavierDo Date: Fri, 4 May 2018 16:19:12 +0200 Subject: [PATCH 1/3] [IMP] mail: add a desktop notification request to systray Purpose of this commit is to improve the onboarding and the first steps of Odoo notably when using Discuss and inviting first people to join Odoo. In this commit we now ask the user to activate desktop notifications. Like already done in Discuss a message will appear in systray in order to ask the user to do it. This message will disappear once the user accepts or blocks desktop notifications. This commit is linked to task ID 1838588 and PR #25075. --- addons/mail/static/src/img/smiley/odoobot.png | Bin 0 -> 10948 bytes .../static/src/js/services/mail_manager.js | 37 ++++++++++++++++-- .../src/js/systray/systray_messaging_menu.js | 23 +++++++++++ .../tests/document_thread_window_tests.js | 5 +++ ...stray_messaging_menu_mail_failure_tests.js | 3 ++ .../systray/systray_messaging_menu_tests.js | 3 ++ 6 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 addons/mail/static/src/img/smiley/odoobot.png diff --git a/addons/mail/static/src/img/smiley/odoobot.png b/addons/mail/static/src/img/smiley/odoobot.png new file mode 100644 index 0000000000000000000000000000000000000000..49513b98ac12abea04ed1a7160af03deef43b2c5 GIT binary patch literal 10948 zcmV;#Dm&GQP)e3HB*4$&r2V}kJ+jX;_P`KTb+G(tBZ3O#pKS9i@_x9)I;z1|=D+*{SqUDb_U zw;QSb`J7Kx-Fxevea`yrHT>3E`@qbY88c&M%#7FQkYDDD`-e4M3w;y3PVr7fS!P5Q zqq-Pm9-tlpR*srpSwN9B=;UL!aC~1Ar5B{LPhj%qog^ znTTF!;9Nz{6`}K3oh zZUx?kU|xNON+VTSr=EWL=KF8kIRk(r9A930*BVc`9pTbn4j1*r1;PmhfG=Nl$JHL@ zc0}Iv%Ndwzq{^4WB)R=W2`Ntq0Nj1Wr_aRm{;!C<<+VV*s4}E#KfYr_?ekr9>1!4M ze|2?NYe$mrQur+apV!(@{ZSlBhN7H0Qk}+cg*n$=Ier7 zIV0Mmg25x9RNwL6{z}NCRcU~`ul&rFV&wlJ(mEr?W1Gutt&XL0Qjq`e=`$!!8c~8-QxfPk+-Z^lE3q~ z0-!x!^k*WRKO>IgHhy>A+B@EPT%5plYd`aDgZZ};J91KnOqu=3FaxzA`YHqL8|$YM zRj?jd54=pCxh=ElEOgKrw9(1O%1}sn&PN}(eMi#(uUWvDHrKPa=awvXPyy$tsEGkRc{@>q#ltws{ZhzKGEF@om@_(4D>o2O7H z(%RZeN9Qc&ch2XO!eSQ0E#w9*rm80@2sV!RdoFn;rRYaph0}F;mnVMa>nD=%tw6FQ z%ji6%TL;;-dl$R5?x1&XH^ak2RLW(dD8kwVRh@E87-I;6fLy*nTU#6R7c64=%9Wh8 z>Qt6iI$>uCq7w%UBAnlrEc#!-C#PKl{Nvg?e*@&NPlO9kj1n%(l09c0dj@;hv~eR_ zwrpnK-X5y8DoGq;tv%dLOD|uU3KMle6kA$YxpEa3Uvx1S%svCQ3=MjD>= z-!|O-{ImgJ-Q`{LC6oWz@m~EFWoSV{?bJNO~;@xO?rVFBkmp9XEBa z2|sHepr>t&{=R)Y{gWr?-g9u*RX&@grC7iZvRGA;B*u6?xqN|^)>c|tTPYNa~7$KX@lF2#{RVo$AV`ZW!Zrr)LwmI$eGkMpg z@8p!;cDylr908#FBdMNxq=h6${;svU}HddU|>o80aIcRWTy?zE6@^tW7`!5kXaX?%6Hu-nEkpFL*OI z%(;}dq2p8eCdjjRM~|9v9S;4Dg^n%Ym^GUfB9`2K)Cn)bOD^ zdI46hdL1AApTEgFuDP0p^XE9N;7^TpRS3g~(%2Y-L&Nm+?ql1wZT$4PpYqJJn|W^Y z({y+5CXN$#<0f>YPn^q&m8Vh7m-*+0OJ`l znGE@Sj$)xmOR+$qkYh#1$*fwjjEl}co8ggBUVL#I|M-=!@UQ>;PYe$Yp)XlI^|`1j z+qS;I-wb_`8_#ZI_lyh*J35Es-_~%Ey!N^%}0hC<2#$~rE=OBH_o}NsUQ#}kDvPk;P+p#4{$s@|D9ud`AW}s87vJm zI5>#47Gq2!1UtOy2SqU{s&hbIAghoO$O1ls5qHt%r57)dq#7hqh*2U1&14DM+6da( z=o=hj|L_or>dSn9L-_*rk;!Da@eS8<^{fk}aJbaZyIYsU_Hx_1*#076~mJALuD4~zT$3Xlj522oxT5@s_Q zIyz?))+!8-3=@VmzPaUI+RrZX=C(7MX$A^jPHqAI^r$t!-(A%;w?KYRv+Da2oAA}% z@AB;a7w8)t#2CZ5=Uv2yK70$SSFh%Q4G-`yUs=bF?b{F10IF^^XHg0QMIaAkMDPUy zRkFoaPQU8yod4Dq#9>4j*68cq%cckJqi6F^h@*&Dg#;x=h!ko- zMU^q2lKyRRTT?ZJpUrXJg>UAHtFB_tu3fCZ?>ltw+2eSMe4dY=`%#t^7B^7|u5cN#eW$OG6=77B3d%&C)Y4Mq+zT$?1Mht=W7P_|LXmIX^L2WA_E4!* z_`A(t=YO65>lD0Plav4}t{ysX%{|<+{*ZZ&!~dgsU$cbY7Z3BJ1Doj|7^D`4ELgOd zcf9LH-uaGe=xA%<@r_S%?|=L|dv@~O}Lr#RODA~FhFc8Ec;m{ys z!$Xu09H2Zr!r0(2V}pZ~28S6N7^XZpOm%n|uUaDqh__%VL?$Ro_3JC|ENd=uBc^IgoHHy?n#2ln&T7rxU}SHKhdfur&PzHrr_ zENfP!k5$X}t4}d9dVtZD*6V%FV{lSC$zgmTCu7I%#*aWo}wHBKmqr=1WZQexQM)*dE zRf&w?sS>I>AsayyJXPmMq$a2ggg{uU@x*jnN!qF|I*gizh;4iibufQvE=5TyoIOx+W8-e8DQtJT>2 z)RTPa3kg=0>#w_x4}Iu^^!M%Kk%u3mQmgTm=fBPWKJVA@o6#&S(mK0!-rIq19jOm+ zbu)w?t7TKNmBGP5s?{njEiJt1;!C;o;)@8wko)gn&%b~39(M2Eg{m@l&Rpis>ZH`y zhaDcGfD?W;UG154^=C|ah>|tp-nS8cHNqG0Mex$qDSNJGC5Q`Ea_##ABF&-&* zAYdoB&vruYPc;oi9XO&W;@M}OX5Bx05#RH;>Z&!o=e_S|U-urKdG;B4MtXU4cngI1Q_qlVs@>5-qaYu65oF*%MJhKz2q`EM)vH%tCq=$IH8M@6(KvpcIP~28gfR+ z86o2dIpdM@3|Y^R@jL=!2#mou;H5(M5Ik|5fl0l58DHdx0xIO@&S&9iXVN-vzI#88 zh?9gkRSMQx2KxKx-Pgw7Xd!9yyk3yxZqn`H+*(V z0KDh4w_PSiZkhJ559Q(H8Mg1`9+__Z2Y3>h;)_(sTkLS7(mgo1AE?TpO z`O8;OiE51O>m`h1lGMOho6y&{kNv&-nZIx``C^ghpM9Fa!9i+qjin2gvN)L23_qat zx)pEz#<#aU)HkUKP$O?{M(U#18)B$3%)sCP0Bs$eyy5I~h~tE>{p-K+;D6prX>^2K zHp^M(p3n8~yq?#edJ0D1yz|dtWaz(A>glHU@kjBJkU(evAfNusdWJ%frI0U@&*dhp zc+b(m4IjY6W-`P!N0KOE6j82KsMRWXVT5N=`q+Y`&R=HMEKXi=3QJE~%$x=DS+aB~ zJ-c?XZPO;IS|Jgon)(S*81dv2KPFMjocW6=jgGoSkR*I(>qDG-<|<4Rw1B7P65!cM z0bpp(CXGA%0S1Q$85=7BFmL{RIyz^w{(%Sh_P^ajZ?9{8vq?hF&TTyM{U4CeWm&v% z0sHp$vUlfBB&-n_#kX$NXN@)hkT*)s^C5Oqw|q6j2A6WPcA&eDmw<*zyw=ta=@h2-~0Bh?L9Z z#rXlTLYx|pT3F-BjXx%n%}}k3*D~zs@1aMAS)$Y5Se5R>__@h7fatl?UDgy5v)2sK z-`Ce*&@w@m%};M;^QIrM?SN9`-SgO)9pK^0)BNxU4^ykTt?}l}oZwMb? zU~qte!GZAvOZvKF^n@%5VGSGAQj*8T{Tbs}zq~*p2$~}itCh*4WQ-xylmbrY;)~*mk4TJSu?U))0t+P89g1Mn1XMO9 zs%oh=u#+TS`uh7A$d;I2X`OBjU}o#AGk_l-#srvE)6V>18OenV3=AAHwOg;(S5-3V z(z;m`znEQ5kbP5F>>;BPoR>Ci6mrbaB!-yg#dN78^Nz9 zWCZ+FppNWu+M&IplO?OqWbR2P(-MXB?c0S8cBf{*q=KiR31qm3WVjo}6_jQ?SIOhy zq61^Z6@98fitThPI*B&%7#rA2R2s$_OQb?*xo<7-bH2r@uC#eI2v=Tls^ z>J3eB17!K(oj{e7r@MTulnp7BN|Z{YQ%rPx75So$q=AMO2#nJLxzzdNzyQ6EJ%HW$ zJZ88L85?p0kY~u}TJhov0g2NjfG2_&Nqd99roKRe;7dRlM~HQdMXhfK`@XlInxDgt z4v{s^yAL(>^o^t*ztdE4N>>Nv% zgyFuS$$Ruv@o*{E0ClTAUHt*-_^XH(iOac(O2X_H?VDl3!zRXzHE?4<~G2QgteB z)p}Al1L|%(&IiaCAuvLKCF8l@^R2UKUAPi&YzVOtfq`6L$mb22g2Br=EudCOYL;qQ zsE#F6qYBxMB@kB7(P0u1GM*t82ar(Hb6E#Qzi}9RtedB5T96`S!~uFLGtgTbWPv}; zU3V(84+{W=S<}q|mZ&l^JbXw8aAIug_;|&nLN_9*eWn3|z@#_XxSL-u|8m(tLnf2Q z4(>*Vy72{aflz2Mn-$z*eXdQ#q1)GAbrob z;EQcIslEuF3Pu!@YBCRTCO~x3*0J5AdwFB$^66fKhX+72Hg>FOM7dI)ELKGYr%KZb zoO%(bNwt7q7X;!=0d=8_r-C!0Q0hZV2|keZ4Y{Hr+hXwA3{o(V^S~1%QM8)idx{t( zQGkF-_Gq#Hko#&`(gL6A{0d4pF> zh2QEy&LF;^Nt*aa@nR(xDq&5DYso>tGe$7hHfjPqz!(sf@l4^%gzIVsj1?w#1EGIx zxCv~aW)7zQ4~ECI*8o#{`5TK%E412 zK|MiUSAeyh=qZJ?^3ypN#uq&A;Qx0ZkjC$ls&1)EIt6YWnDwbLf!TDkJ^UDp%1uxJ z2g~@SJV3-WR{|R=Q@40E>g=6ApsJ|2670s(64GL>gIhcr_k6<5w<2z%2bWo(NP?o0=P|*hK3mNxxlR22xbhgz`AiD~(U9`$*aZ6l#x~6n zAmi6_sFC-w(=LBi4JUH~s~|1YIRPJo?*~(Gfa(M%nhJQrYf36?ClpmBNpHBdbvib^ z=~7>z5JgJ0Y%#vU^U}h;$RH-o_&HN9KWVUqDfQc65VbKU2a0;4 zs6PLCDWOd2W9Nfx6EuKS4$lNCBM8%4pOn)KnM`&n1u(vXQ$bsm2G2gRcQ3}0U`R9# z*T8l3sQaHdE$xaDsFf{&7bC_)tU954;-p(ubj(sKB~;4^aXeAGXxbqJ5lURw0`8e_ z?)5ql?ZHA%)Ln~Z>ZaEAOw05G03Do!2$Dg8mD73pV&Z!7>lgkqe;{@El7m*2r0N}F zRVQrgO1C3xsad5aif@H5ZRBw8(I8^ninmA!tc4&_Ovb}_>V#Ic)XE8!vZWTe^Mq+# zK$IwxCTG(SIk`V{~SPLd0tf4V2oYI76{ zg{h+j(wi^NDY28j(#;(F=pHW zQv{76OcG2r#$d46)X@`?RLw`JaHGVXqo$f3P*t4PN$LPeL1Q&FKcw2qfh0spn0jN0 zg@w7zhyr#^5(ctsy3@h)wVhm$A(P2atJMw*1gj8=3riw(g`bJ3+hOpm%L9bs1QJ-8 zkdzVpMtx42@>Rha8eY0F_3og=y(iKTTBHzJOQ=Fsm8!MrPj}R;QdOmP8RT0)I5|V=%_nIoSgh{5Yib)pFQ;{Z zbs&^2>F+{Kok^${g2vXFgb=8uV3=yGp0k3i38r8EVz(XM3AFsI8M~M)^5@Xj)b^>OFlPN|-Woo(i5-D)j_Xnk_UFxH(l&;{+6l^V}QEp#ilMqKk9| zI{-i>aT=y<-QECYtCX!YfYERiaV8^HNrC3fMa}6LrrKodjM=Mb#)w6pIV=E@%D}Ty zoTW0&TY@d~=F{0Z8{glRmPyMYZ6Y|;T4j8T7cX_H#H6&Wstf7r6s;%{JCfUaIgSRAg$1bG?s_LqNtJX4>Sjq{MtvC%}p_T$APMyU#{r{>6)l>^i z=KZTGnM{T=J61PC{g?NwuWdVQF5n}N{Amef`*hD^4sD#=xrDa%_9^**7^ngjlp2;W z4G$wb?gUoTkiL?@SS*YsO35mtiE<#Z=}-4}$tt5Zp_EukiBd{%ucv>HCYA%SQi_#Q zl2D4rKR4#MA}Wcdf*0qgGVTn9>NHkWm1@cvsOY5e|BBfsvoJTO3F=?BbkU`gFoA&b z1j35xp3G{03D3-#%iuu&VR!$MRE<~Du3ca2aeX~mxw)wB>=GIN6qL~&J*y)meuT!>oC68+Xcjt3FK$=sia||itR|Bev13^9f3+p_9Iy2Pp z_EX_k6e_7%V2V8rMT9lW-%QcVH(~LsYFPc@lmMs=ZQYccy>g@(&jdI%S;SK(EoWbE zFXi&s;XxoSq^s(L!jK8&0fJ&h(rlcg4Q(T&bKAVcDLWV#XHM#sKJ^_!>KVScLF_oz z&yhYfqJl7GAnU?+0F%^D8Q&^Kg(mTL0Ll&&Q}f*o-+9 zZHBWuPi4WP#mFS_XFZ2*fvUh5>b7Z1w`v<_f?Q%V(8Q3nhzl=2C~Qlqh4? zQnH%9rtWo%d(EbH-_}>XI1pGRLaE^*lrl;gD5uPKmB9c23PnjoK~z9Urttj1J%!|L zr(RCGDK<&|dhemLvxmCj%6$m$ZpOq;t1jS~lUA^|yPHa7Y;q8!{y@22VKu>@4XmId z*p%{3)TM=esV8V$7+7ZtO?q2QBWdH(#&PO1G;$8sQMoQ3Amd;oH4$m+*O)>z&0q>s ztNxCbcCMOx0nHj>l5Za|0BSb*c1FCv7n%MYjB+;M()P3HfBgVYKlNnVvVAyzz)3y< zDy~v=+~J#0b)%|j>ezJwS(~zlsuP&P`gXB(pP`=DPt(ZMGh?9h|fB4J=x?;E0~; zC24q9R%H|zO|?QL^%H7!ejxSQD=CY2aGSR_RDRjk89ve@4gIVhR=>j65BHuwiOu-BAZwa2L4)DE$};w!b0H%m4@}uN z=$ba2CaBh%GV6sG;>y4RlsMfJAW2)jeMvKf@{;P;dhH>4NB32h<-4~EXNH|`dQ?Dq(b@tYOXFQ)@Kf(s%=u~`FlrqAY>bSRG z2jme5zfdUh8*jR)Dcix+MO{_k|dcF1ohHv6~Q9JX+FPEvnQ!`fP-rJk_paUq7(P?Nz=M%8n+(v*TcK9^mk|P zrSZ(5sbsIJWOG^GbJ6u&U{=%2@xsu~&;D@lrpJ!j1OPm1kYfTZ6NjDtuL5%(X81tI0kysh9ul?GN_euyV}>f~;!Rqz%Hn zN)dlm2a&P#@9!hA$&o#LNyekYWP%#63FG_z;5hp_NNOp6UroVNuCI2Wl>V#>zp+W^ z6mzLZF^b~(9&bA10^T=g4H^8V0syhU|Jx7V{^VpInQ}%6e9DJwnmKRZ8qV3ji2MbY z<9h*5J-LxEtZ^iUw%%x|ZWI3ozhA4jcd6U>(KJkm)^~Yzt3P!j6ULi68a~GqkF1%< z@_~CE(M4xm$Oq?NO;&^E3SZe#9o+s;Q+;g8IRD?bZrF3f$~A8hWJNRQ?5p`rzJou3R%$L@qd9mll*^&TxhW%a$mrR6S_XxA+Q!m1 ztYq=(jjZmtD%OkhX$1hl-D~gMAjp-RK%>?nlor5n zd&usd-MskX3+&jjoxy=Vs@1C7H^Az+vV=xYf*y>L2@atU1X)^J+n6x$fo-x83^+w8qhoFRlI55})8Hny|0nQG39i63+K2cfwG%%>L3K z`}gl-ps$aC!2wF6qtt3{pCX&o`*z*dGM?{|$z;go^0cZb0YgD2t zk;abei(G!dw}zt4QVcWXW1nI*pe4?n$d`wrk8R&my|nAOuBfR1NKL^Vf_$8raeRcY z45_DQ-+cdVuehi47mDe(ht=CrzCR<1<74;1-tvnG{}2*LF_xzZmF4ZY;68Fw?);fkCx^=E&u?y zY5ga@8E z{jIl7=Q*aexZ|b=Kk@vR*Xk6qod;%+bBnMJtx zM9&?Z0K$JhQC)lU10P@CjMFt$7++p{*BVdtpAcCvBk4mrdtWU@)sOx9!(C0|Q<}aS zzi`E;=Vd*A9m3T!B5&CG5!C+PO%HthADVscrYE<*v;Cp5v$kFGwf2?%R#Yw$q4l-B z>iJsL#Mggh{U?5KJkEby?eSk--PPI==Wa*%ZIIwKw(Os`8vpvu8$S8a@tnp9;gGFc zd&lcU&0mVh+g}5cA6Dg0NBr(TKh~_?s}=waC-KTVE-}Wt4bf|U38RGpMgO8|`Rk8u zxb47+GN}{tCVzR&oo5)~4-xsVLbF?HkGX@lPgVbXEUNx(Grjv)H2~`4i&uPVsqY29 zDZ+;Y=D)fGuTO~ObCsd$S3kC?t8${w^Hrf{KX>l0`T5Qj*Qnuxz%_ytPf)RUt8!10 zB>#BJL!bP~tKr_h>g?=iFZ!L_?ACd2SCMN)xk^M%KE5lREx>&?Nxrl5!SIn|nX8`x zz!40WaFR;|b0KhnDyPeIJ^fTEtLPR*9}{>qwzVI8WJA~9SM&Y*rQ_v4ckZwGUgt@t z_{Kj?UA=)WQCr|`3{y2kN*!zcaGo25)yI%0000 inbox > channel, each group must be sorted previewsChannel = self._sortPreviews(previewsChannel); previewsInbox = self._sortPreviews(previewsInbox); previewsFailure = self._sortPreviews(previewsFailure); - return _.union(previewsFailure, previewsInbox, previewsChannel); + return _.union(previewsFailure, previewsInbox, previewsChannel, previewsStatic); }); }, /** @@ -797,6 +804,28 @@ var MailManager = AbstractService.extend({ return $.when([]); } }, + /** + * Get the previews of static systray elements, adding notification request + * + * @private + * @param {string|undefined} [filter] + */ + _getSystrayStaticPreviews: function (filter) { + var elems = []; + if ( + window.Notification && window.Notification.permission === "default" && + (filter === 'mailbox_inbox' || !filter) + ) { + elems.push({ + title: _t("Odoobot has a request for you"), + imageSRC: "/mail/static/src/img/smiley/odoobot.png", + status: 'bot', + body: _t("Enable desktop notifications to stay up to date"), + id: 'request_notification', + }); + } + return $.when(elems); + }, /** * Initialize the canned responses from the server data * diff --git a/addons/mail/static/src/js/systray/systray_messaging_menu.js b/addons/mail/static/src/js/systray/systray_messaging_menu.js index 8ab95cc6fab..5552a80393e 100644 --- a/addons/mail/static/src/js/systray/systray_messaging_menu.js +++ b/addons/mail/static/src/js/systray/systray_messaging_menu.js @@ -150,6 +150,27 @@ var MessagingMenu = Widget.extend({ previews: previews, })); }, + /** + * Display the browser notification request dialog when the user clicks on systray's corresponding notification + * + * @private + */ + _requestNotificationPermission: function () { + var self = this; + var def = window.Notification && window.Notification.requestPermission(); + if (def) { + def.then(function (value) { + if (value !== 'granted') { + self.call('bus_service', 'sendNotification', self, _t('Permission denied'), + _t('Odoo will not have the permission to send native notifications on this device.')); + } else { + self.call('bus_service', 'sendNotification', self, _t('Permission granted'), + _t('Odoo has now the permission to send you native notifications on this device.')); + } + }); + } + this.$(".o_mail_navbar_request_permission").slideUp(); + }, /** * Get and render list of previews, based on the selected filter * @@ -269,6 +290,8 @@ var MessagingMenu = Widget.extend({ var documentID = $target.data('document-id'); var documentModel = $target.data('document-model'); this._openDocument(documentModel, documentID); + } else if (previewID === 'request_notification') { + this._requestNotificationPermission(); } else { // preview of thread this.call('mail_service', 'openThread', previewID); diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js index 8cae9862dea..14d50d593ba 100644 --- a/addons/mail/static/tests/document_thread_window_tests.js +++ b/addons/mail/static/tests/document_thread_window_tests.js @@ -84,7 +84,12 @@ QUnit.module('DocumentThreadWindow', { partner_id: partnerID, // so that needaction messages are treated as needactions }; this.services = mailTestUtils.getMailServices(); + this.MailService = this.services.mail_service; + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = false; }, + afterEach: function () { + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = true; + } }); QUnit.test('open a document thread in a thread window', function (assert) { diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js index c1fc1bd8dae..0ef6679c675 100644 --- a/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js +++ b/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js @@ -58,8 +58,11 @@ QUnit.module('MessagingMenu (Mail Failures)', { }; this.services = mailTestUtils.getMailServices(); + this.MailService = this.services.mail_service; + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = false; }, afterEach: function () { + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = true; // unpatch _.debounce and _.throttle _.debounce = this.underscoreDebounce; _.throttle = this.underscoreThrottle; diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js index 634eef57d18..8c2942be76d 100644 --- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js @@ -93,8 +93,11 @@ QUnit.module('MessagingMenu', { }, }; this.services = mailTestUtils.getMailServices(); + this.MailService = this.services.mail_service; + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = false; }, afterEach: function () { + this.MailService.prototype.IS_STATIC_PREVIEW_ENABLED = true; // unpatch _.debounce and _.throttle _.debounce = this.underscoreDebounce; _.throttle = this.underscoreThrottle; From c446fb20015d489d9c38e97e10f25cf2e5e3efd7 Mon Sep 17 00:00:00 2001 From: XavierDo Date: Mon, 7 May 2018 11:36:30 +0200 Subject: [PATCH 2/3] [IMP] auth_signup, mail: notify inviter user when invited user connects Purpose of this commit is to improve the onboarding and the first steps of Odoo notably when using Discuss and inviting first people to join Odoo. When a new user connects the user who invited him will be notified with a desktop notification inviting him to have a chat in Discuss and talk together. This commit is linked to task ID 1838588 and PR #25075. --- addons/auth_signup/models/res_users.py | 19 +++++++++++- .../bus/static/src/js/services/bus_service.js | 11 +++++-- .../js/services/mail_notification_manager.js | 29 +++++++++++++++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py index 598d390dbd5..27fd43a0403 100644 --- a/addons/auth_signup/models/res_users.py +++ b/addons/auth_signup/models/res_users.py @@ -81,6 +81,8 @@ class ResUsers(models.Model): values.pop('login', None) values.pop('name', None) partner_user.write(values) + if not partner_user.login_date: + partner_user._notify_inviter() return (self.env.cr.dbname, partner_user.login, values.get('password')) else: # user does not exist: sign up invited user @@ -92,7 +94,8 @@ class ResUsers(models.Model): if partner.company_id: values['company_id'] = partner.company_id.id values['company_ids'] = [(6, 0, [partner.company_id.id])] - self._signup_create_user(values) + partner_user = self._signup_create_user(values) + partner_user._notify_inviter() else: # no token, sign up an external user values['email'] = values.get('email') or values.get('login') @@ -110,6 +113,20 @@ class ResUsers(models.Model): raise SignupError(_('Signup is not allowed for uninvited users')) return self._create_user_from_template(values) + @api.multi + def _notify_inviter(self): + for user in self: + invite_partner = user.create_uid.partner_id + if invite_partner: + # notify invite user that new user is connected + title = _("%s connected") % user.name + message = _("This is his first connection. Wish him welcome") + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', invite_partner.id), + {'type': 'user_connection', 'title': title, + 'message': message, 'partner_id': user.partner_id.id} + ) + def _create_user_from_template(self, values): template_user_id = literal_eval(self.env['ir.config_parameter'].sudo().get_param('base.template_portal_user_id', 'False')) template_user = self.browse(template_user_id) diff --git a/addons/bus/static/src/js/services/bus_service.js b/addons/bus/static/src/js/services/bus_service.js index 8e367b7f93a..af505dfc6e4 100644 --- a/addons/bus/static/src/js/services/bus_service.js +++ b/addons/bus/static/src/js/services/bus_service.js @@ -27,11 +27,12 @@ var BusService = CrossTab.extend(ServicesMixin, { * * @param {string} title * @param {string} content + * @param {function} [callback] if given callback will be called when user clicks on notification */ - sendNotification: function (title, content) { + sendNotification: function (title, content, callback) { if (window.Notification && Notification.permission === "granted") { if (this.isMasterTab()) { - this._sendNativeNotification(title, content); + this._sendNativeNotification(title, content, callback); } } else { this.do_notify(title, content); @@ -76,8 +77,9 @@ var BusService = CrossTab.extend(ServicesMixin, { * @private * @param {string} title * @param {string} content + * @param {function} [callback] if given callback will be called when user clicks on notification */ - _sendNativeNotification: function (title, content) { + _sendNativeNotification: function (title, content, callback) { var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"}); notification.onclick = function () { window.focus(); @@ -86,6 +88,9 @@ var BusService = CrossTab.extend(ServicesMixin, { } else if (this.close) { this.close(); } + if (callback) { + callback(); + } }; }, diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index b3e5239f9ee..475cbd44f90 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -165,13 +165,13 @@ MailManager.include({ * Add or remove failure when receiving a failure update message * * @private - * @param {Object} data - * @param {Object[]} data.elements list of mail failure data - * @param {string} data.elements[].message_id ID of related message that + * @param {Object} datas + * @param {Object[]} datas.elements list of mail failure data + * @param {string} datas.elements[].message_id ID of related message that * has a mail failure. - * @param {Array} data.elements[].notifications list of notifications + * @param {Array} datas.elements[].notifications list of notifications * that is related to a mail failure. - * @param {string} data.elements[].notifications[0] sending state of a mail + * @param {string} datas.elements[].notifications[0] sending state of a mail * failure (e.g. 'exception'). */ _handlePartnerMailFailureNotification: function (datas) { @@ -313,6 +313,8 @@ MailManager.include({ this._handlePartnerActivityUpdateNotification(data); } else if (data.type === 'mail_failure') { this._handlePartnerMailFailureNotification(data); + } else if (data.type === 'user_connection') { + this._handlePartnerUserConnectionNotification(data); } else { this._handlePartnerChannelNotification(data); } @@ -402,8 +404,25 @@ MailManager.include({ this._mailBus.trigger('unsubscribe_from_channel', data.id); this.do_notify(_("Unsubscribed"), message); } + }, + /** + * Shows a popup to notify a user connection + * + * @private + * @param {Object} data + * @param {Object[]} data.partner_id id of the connected partner + * @param {string} data.title title to display on notification + * @param {Array} data.messages message to display on notification + */ + _handlePartnerUserConnectionNotification: function (data) { + var self=this; + var partner_id = data.partner_id; + this.call('bus_service', 'sendNotification', data.title, data.message, function(){ + self.call('mail_service', 'openDmWindow', partner_id); + }); }, /** + * * On receiving an update on user status (e.g. becoming 'online', 'offline', * 'idle', etc.). * From cf505c36985221819139ce30d7f03e6e51ec0298 Mon Sep 17 00:00:00 2001 From: XavierDo Date: Thu, 2 Aug 2018 13:18:53 +0200 Subject: [PATCH 3/3] [ADD] mail_bot, im_livechat_mail_bot: add Odoobot, your new best friend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Purpose of this commit is to improve onboarding with a wow effect and a bot to test the Discuss app. Otherwise new users have nobody to talk to. Retention will be improved by both increasing interactions and onboarding of Discuss features. This commit adds a simple bot in discuss. It answers some questions, helps users getting their hand on discuss and eases the onboarding. In this version, the flow is quite simple, and only im_livechat adds some logic in order to show canned response. The logic is contained in new modules: mail_bot and im_livechat_mail_bot in order to keep everything well separated. It also allows users to remove the mailbot if they do not want to keep this functionality. Odoobot will only answer if he is in the onboarding conversation (alone with a user in a channel of type chat) or if a user pings odoobot. Odoobot logic applies to both standard chatter / channel messages and also transient messages (like help commands). Specifications * 2 minutes after first sign in, users will receive a direct chat from Odoobot; * make Odoobot an archived partner; * scenario * Odoobot: "Hello, I'm here to help you discover chat features. Try answering me with an emoji :)"; * User: Send emoji * Odoobot: "Great! :) Did you notice that you can also send attachments, like a picture of your cute dog? Try it!" * Odoobot: "Not a cute dog, but you get it :) To access special features, start your sentence with "/" (I.E. /help)"" * User: /help * Auto message then Odoobot: "Wow you're a natural! As a channel usually contain a lot of users, you can grab the attention with a ping. Try to ping me with @Odoobot!" * User: @Odoobot lorem ipsum * if Livechat installed * add 2 demo canned response so it does not look weird (like "Hello, how may I help you?" and "Have a nice day!") * Odoobot: "Perfect!
Try to type ":" to use canned responses." * User tries canned * Odoobot: "Good, you can customize your canned responses in the live chat application.

+ rΓ©ponse suivante" * Odoobot: "There's 3 different ways in Odoo to interact with your colleagues: via this chat window, [img of chat window] via the Discuss application [img of Discuss app + icon on it] or via the chatter [img of the chatter]. Aaaaand that's it! Enjoy discovering Odoo! :)" * random answers to ping/bad answer * "Mmmmh I'm not sure what you mean.. Can you try again?" * "I'm afraid I don't understand. Sorry!" * when someone pings @OdooBot with no reason: Odoobot: Yaaaay that's me! [party emoji] * fun stuff to add for some answer * User: i love you / love * Odoobot: Aaaaaw that's really cute but, you know, bots don't work that way. You're too human for me! Let's keep it professional <3 * User: Fuck * Odoobot: That's not a really nice thing to say, you know? I'm a bot but I have feelings, ok?! 2215 bytes .../static/src/js/composers/basic_composer.js | 1 + .../static/src/js/models/messages/message.js | 2 +- .../static/src/js/models/threads/thread.js | 1 + addons/mail_bot/__init__.py | 4 + addons/mail_bot/__manifest__.py | 22 ++ addons/mail_bot/data/mailbot_data.xml | 12 + addons/mail_bot/models/__init__.py | 9 + addons/mail_bot/models/ir_http.py | 13 ++ addons/mail_bot/models/mail_bot.py | 216 ++++++++++++++++++ addons/mail_bot/models/mail_channel.py | 41 ++++ addons/mail_bot/models/mail_thread.py | 12 + addons/mail_bot/models/res_partner.py | 26 +++ addons/mail_bot/models/res_users.py | 9 + .../mail_bot/static/img/odoobot_chatter.png | Bin 0 -> 40576 bytes .../mail_bot/static/img/odoobot_discuss.png | Bin 0 -> 30404 bytes .../mail_bot/static/src/js/mailbot_service.js | 28 +++ addons/mail_bot/views/assets.xml | 10 + addons/mail_bot/views/discuss.xml | 13 ++ addons/test_mail/__manifest__.py | 2 +- addons/test_mail/tests/__init__.py | 3 +- addons/test_mail/tests/test_odoobot.py | 124 ++++++++++ addons/test_mail/tests/test_performance.py | 24 +- 30 files changed, 615 insertions(+), 17 deletions(-) create mode 100644 addons/im_livechat_mail_bot/__init__.py create mode 100644 addons/im_livechat_mail_bot/__manifest__.py create mode 100644 addons/im_livechat_mail_bot/models/__init__.py create mode 100644 addons/im_livechat_mail_bot/models/mail_bot.py create mode 100644 addons/mail/static/src/img/odoobot.png create mode 100644 addons/mail_bot/__init__.py create mode 100644 addons/mail_bot/__manifest__.py create mode 100644 addons/mail_bot/data/mailbot_data.xml create mode 100644 addons/mail_bot/models/__init__.py create mode 100644 addons/mail_bot/models/ir_http.py create mode 100644 addons/mail_bot/models/mail_bot.py create mode 100644 addons/mail_bot/models/mail_channel.py create mode 100644 addons/mail_bot/models/mail_thread.py create mode 100644 addons/mail_bot/models/res_partner.py create mode 100644 addons/mail_bot/models/res_users.py create mode 100644 addons/mail_bot/static/img/odoobot_chatter.png create mode 100644 addons/mail_bot/static/img/odoobot_discuss.png create mode 100644 addons/mail_bot/static/src/js/mailbot_service.js create mode 100644 addons/mail_bot/views/assets.xml create mode 100644 addons/mail_bot/views/discuss.xml create mode 100644 addons/test_mail/tests/test_odoobot.py diff --git a/addons/bus/static/src/js/services/bus_service.js b/addons/bus/static/src/js/services/bus_service.js index af505dfc6e4..72ba8c1531b 100644 --- a/addons/bus/static/src/js/services/bus_service.js +++ b/addons/bus/static/src/js/services/bus_service.js @@ -80,7 +80,7 @@ var BusService = CrossTab.extend(ServicesMixin, { * @param {function} [callback] if given callback will be called when user clicks on notification */ _sendNativeNotification: function (title, content, callback) { - var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"}); + var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoobot.png"}); notification.onclick = function () { window.focus(); if (this.cancel) { diff --git a/addons/im_livechat_mail_bot/__init__.py b/addons/im_livechat_mail_bot/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/im_livechat_mail_bot/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/im_livechat_mail_bot/__manifest__.py b/addons/im_livechat_mail_bot/__manifest__.py new file mode 100644 index 00000000000..7c1906b78ea --- /dev/null +++ b/addons/im_livechat_mail_bot/__manifest__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Odoobot for livechat', + 'version': '1.0', + 'category': 'Discuss', + 'summary': 'Add livechat support for odoobot', + 'description': "", + 'website': 'https://www.odoo.com/page/discuss', + 'depends': ['mail_bot', 'im_livechat'], + 'installable': True, + 'application': False, + 'auto_install': True, +} diff --git a/addons/im_livechat_mail_bot/models/__init__.py b/addons/im_livechat_mail_bot/models/__init__.py new file mode 100644 index 00000000000..ce6a8455179 --- /dev/null +++ b/addons/im_livechat_mail_bot/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import mail_bot diff --git a/addons/im_livechat_mail_bot/models/mail_bot.py b/addons/im_livechat_mail_bot/models/mail_bot.py new file mode 100644 index 00000000000..57eacc5db5d --- /dev/null +++ b/addons/im_livechat_mail_bot/models/mail_bot.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, _ + + +class MailBot(models.AbstractModel): + _inherit = 'mail.bot' + + def _get_answer(self, record, body, values, command): + if self._is_bot_in_private_channel(record): + if self._is_bot_pinged(values): + return _("That's me! πŸŽ‰
Try to type \":\" to use canned responses.") + elif values.get("canned_response_ids"): + discuss_href = 'href="/web#action=mail.mail_channel_action_client_chat&active_id=%s"' % record.id + discuss_src = 'src="/mail_bot/static/img/odoobot_discuss.png"' + chatter_src = 'src="/mail_bot/static/img/odoobot_chatter.png"' + return _("Good, you can customize your canned responses in the live chat application.

") + \ + _("There are 3 different ways in Odoo to interact with your colleagues:
\ +-via this chat window
\ +-via the Discuss application:


\ +-or via the chatter:


\ +Aaaaand that's it! Enjoy discovering Odoo!") % (discuss_href, discuss_src, chatter_src) + + return super(MailBot, self)._get_answer(record, body, values, command) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 4c46c8b10cc..6f6e53da75d 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -828,6 +828,9 @@ class Channel(models.Model): WHERE C.uuid = %s""", (uuid,)) return self._cr.dictfetchall() + def _channel_fetch_listeners_where_clause(self, uuid): + return ("C.uuid = %s", (uuid,)) + @api.multi def channel_fetch_preview(self): """ Return the last message of the given channels """ @@ -888,7 +891,8 @@ class Channel(models.Model): if self.public == 'private': msg += _(" This channel is private. People must be invited to join it.") else: - channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) + all_channel_partners = self.env['mail.channel.partner'].with_context(active_test=False) + channel_partners = all_channel_partners.search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) msg = _("You are in a private conversation with @%s.") % (channel_partners[0].partner_id.name if channel_partners else _('Anonymous')) msg += _("""

Type @username to mention someone, and grab his attention.
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 1288f780d49..f086a3220a7 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -2030,7 +2030,11 @@ class MailThread(models.AbstractModel): values.pop(x, None) # Post the message + # canned_response_ids are added by js to be used by other computations (odoobot) + # we need to pop it from values since it is not stored on mail.message + canned_response_ids = values.pop('canned_response_ids', False) new_message = MailMessage.create(values) + values['canned_response_ids'] = canned_response_ids self._message_post_after_hook(new_message, values, model_description=model_description, mail_auto_delete=mail_auto_delete) return new_message diff --git a/addons/mail/static/src/img/odoobot.png b/addons/mail/static/src/img/odoobot.png new file mode 100644 index 0000000000000000000000000000000000000000..b1921736d32fb142e50c200d14900cd9452b7f3b GIT binary patch literal 2215 zcmV;Y2w3-tP)QiOP^NMH_)n5h5WpP%4-D& zZLOzcdAtH*QxXy%PePO;JsrzPj)RuhODN>lY+Jd!@&)`zWoMAMJn87Tj)Cqs;7&OW zyyu`*rHa+RRt#@u{Z>#6;mWmi&rM3tKx=Ich1@d~pT6ux_qHoJcM+-VY|p%vo`7!D z>c}rH4UCy!l`r5&=Jo={!sE90Z98ary@ZwD^OpMXcm<4w$015bKJj^>t8c9P+}fKX z-&?7oQL9__V$wG=w%cx7L9aZ_LEhLj^<7`Uk7!at;_{^B71gNKA+MLPRa8v>s}j=_ zNPm&FbREZK8)&nT6Vnqt^>0kvMwIj#iuW!Dpjujxj?V}wL)#B%h@u3n-JGVG$w@u01tQY6DB=H6`;%fv71X;&=L+8VZsWwdtw zxA$T^;+Sn$l`7Qo|F+aO>PIvgXY%<)DFR=>PrO%c70blbGa_i3k&iOuv=P$|@`pdx z?5F9T<;PWu3Z6Y#=?>}lJad{sG$|3o4>VSWrbBCMbZ>(ao7F>*qzi2A?j!&65>}t^ zJ<_sh^~nnIKasA&H!)F)AoI-v4%%3S&6&i^FxDZ)BLE@&xnXieFW-9%00@RdJ+15Ol)eF>hfZ0t zkvc`2vlurHOc{cvJ#HSfH8g5S%#y^|DcW3p0Y7oU5dCOcjCPB%^V?1HcdnmsgAUpn zmhU~r%EO!`Kebb>ojt5P%$+Ko2#O(7W`j0Y8Y|-FY&01s76a*7Iy^hsW8rbk-@SqS z;?jwgEN^V0@>)S`O0rZIM=Em@r3f;&7r0FoOSV=t87F>D1RYCC@OTBHZIYjtxKjr7 z)Yw-~U!ag%`>+3lwg!1)6Y|C;f?^1wR3T&#|htQ5ZAN|avMuBFSpuXlrMW zT6G(vWHJE^RavT1{m&&ck{tz)W-D7s*JMX!E=w)t{GIF8Y&5`@r6X@_TCV5zjgBDw zMHaKy(%34NQ7I}=l`8WNxt>Z8OG?ZcNHnQRl?s_PTZ^gAkfnGtD2DL)PhV39ps;U4pIrSRk4&X`vQJ^`OCLV{V~U*SpXLD zYo;H~2T6@uovBWoXHhYOPF$X3s=-^n_jsr`1+=3FD4K#|h$|53gdAdmI}@19KdkF0%&=MyDiPxO&MwV30@dk(tICnnPf_Md;q!Ltu5e z#~PoOdr 0 ? commands[0].name : undefined, }); }, diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index 9bf7a79d1b9..0c6480f3264 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -97,7 +97,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin */ getAvatarSource: function () { if (this._isOdoobotAuthor()) { - return '/mail/static/src/img/odoo_o.png'; + return '/mail/static/src/img/odoobot.png'; } else if (this.hasAuthor()) { return '/web/image/res.partner/' + this.getAuthorID() + '/image_small'; } else if (this.getType() === 'email') { diff --git a/addons/mail/static/src/js/models/threads/thread.js b/addons/mail/static/src/js/models/threads/thread.js index f1831cd3aa8..5fcd0285a82 100644 --- a/addons/mail/static/src/js/models/threads/thread.js +++ b/addons/mail/static/src/js/models/threads/thread.js @@ -329,6 +329,7 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, { partner_ids: data.partner_ids, body: body, attachment_ids: data.attachment_ids, + canned_response_ids: data.canned_response_ids, }; if ('subject' in data) { messageData.subject = data.subject; diff --git a/addons/mail_bot/__init__.py b/addons/mail_bot/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/mail_bot/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/mail_bot/__manifest__.py b/addons/mail_bot/__manifest__.py new file mode 100644 index 00000000000..01885576d16 --- /dev/null +++ b/addons/mail_bot/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Odoobot', + 'version': '1.0', + 'category': 'Discuss', + 'summary': 'Add Odoobot in discussions', + 'description': "", + 'website': 'https://www.odoo.com/page/discuss', + 'depends': ['mail'], + 'installable': True, + 'application': False, + 'auto_install': True, + 'data': [ + 'views/assets.xml', + 'data/mailbot_data.xml', + ], + 'qweb': [ + 'views/discuss.xml', + ], +} diff --git a/addons/mail_bot/data/mailbot_data.xml b/addons/mail_bot/data/mailbot_data.xml new file mode 100644 index 00000000000..96fe760ca1f --- /dev/null +++ b/addons/mail_bot/data/mailbot_data.xml @@ -0,0 +1,12 @@ + + + + + OdooBot + + OdooBot + + + + + diff --git a/addons/mail_bot/models/__init__.py b/addons/mail_bot/models/__init__.py new file mode 100644 index 00000000000..82aa165ecaf --- /dev/null +++ b/addons/mail_bot/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import ir_http +from . import mail_bot +from . import mail_channel +from . import mail_thread +from . import res_partner +from . import res_users diff --git a/addons/mail_bot/models/ir_http.py b/addons/mail_bot/models/ir_http.py new file mode 100644 index 00000000000..ae01cb3e76f --- /dev/null +++ b/addons/mail_bot/models/ir_http.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Http(models.AbstractModel): + _inherit = 'ir.http' + + def session_info(self): + res = super(Http, self).session_info() + res['odoobot_initialized'] = self.env.user.odoobot_initialized + return res diff --git a/addons/mail_bot/models/mail_bot.py b/addons/mail_bot/models/mail_bot.py new file mode 100644 index 00000000000..477e6aab7ee --- /dev/null +++ b/addons/mail_bot/models/mail_bot.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import itertools +import random + +from odoo import models, _ + + +class MailBot(models.AbstractModel): + _name = 'mail.bot' + + def _apply_logic(self, record, values, command=None): + """ Apply bot logic to generate an answer (or not) for the user + The logic will only be applied if odoobot is in a chat with a user or + if someone pinged odoobot. + + :param record: the mail_thread (or mail_channel) where the user + message was posted/odoobot will answer. + :param values: msg_values of the message_post or other values needed by logic + :param command: the name of the called command if the logic is not triggered by a message_post + """ + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + if len(record) != 1 or values.get("author_id") == odoobot_id: + return + if self._is_bot_pinged(values) or self._is_bot_in_private_channel(record): + body = values.get("body", "").replace(u'\xa0', u' ').strip().lower().strip(".?!") + answer = self._get_answer(record, body, values, command) + if answer: + message_type = values.get('message_type', 'comment') + subtype_id = values.get('subtype_id', self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')) + record.with_context({"mail_create_nosubscribe": True}).message_post(body=answer, author_id=odoobot_id, message_type=message_type, subtype_id=subtype_id) + + def _get_answer(self, record, body, values, command=False): + # onboarding + if self._is_bot_in_private_channel(record): + # main flow + if body in ('hi', 'hello'): + return _("Hi, I will guide you through chat features. First, try to send me an emoji 😊") + elif self._body_contains_emoji(body): + return _("Great! :) Did you notice that you can also send attachments, like a picture of your cute dog? Try it!") + elif values.get("attachment_ids"): + return _("Not a cute dog, but you get it :) To access special features, start your sentence with '/' (e.g. /help).") + elif command == 'help': + return _("Wow you are a natural! Ping someone to grab its attention with @nameoftheuser. Try to ping me with @OdooBot.") + elif self._is_bot_pinged(values): + discuss_href = 'href="/web#action=mail.mail_channel_action_client_chat&active_id=%s"' % record.id + discuss_src = 'src="/mail_bot/static/img/odoobot_discuss.png"' + chatter_src = 'src="/mail_bot/static/img/odoobot_chatter.png"' + return _("That's me! πŸŽ‰
") + \ + _("There are 3 different ways in Odoo to interact with your colleagues:
\ +-via this chat window
\ +-via the Discuss application:


\ +-or via the chatter:


\ +Aaaaand that's it! Enjoy discovering Odoo!") % (discuss_href, discuss_src, chatter_src) + # easter eggs + elif body in ['❀️', _('i love you'), _('love')]: + return _("Aaaaaw that's really cute but, you know, bots don't work that way. You're too human for me! Let's keep it professional ❀️") + elif body in [_('help'), _('help me'), _('i need help')]: + return _("I'm trying to help you, but I'm just a bot... :( You can also check our documentation) for more information!") + elif _('fuck') in body or "fuck" in body: + return _("That's not a really nice thing to say, you know? I'm a bot but I have feelings, ok?! πŸ’”") + else: + return random.choice([ + _("Mmmmh I'm not sure what you mean.. Can you try again?"), + _("I'm afraid I don't understand. Sorry!") + ]) + elif self._is_bot_pinged(values): + return random.choice([_("Yaaaay that's me!"), _("Odeyhoooo")]) + return False + + def _body_contains_emoji(self, body): + # coming from https://unicode.org/emoji/charts/full-emoji-list.html + emoji_list = itertools.chain( + range(0x0030, 0x3a), + range(0x2194, 0x219a), + range(0x21A9, 0x21ab), + range(0x231A, 0x231c), + range(0x23E9, 0x23f4), + range(0x23F8, 0x23fb), + range(0x25AA, 0x25ac), + range(0x25FB, 0x25ff), + range(0x2600, 0x2605), + range(0x2614, 0x2616), + range(0x2622, 0x2624), + range(0x262E, 0x2630), + range(0x2638, 0x263b), + range(0x2648, 0x2654), + range(0x265F, 0x2661), + range(0x2665, 0x2667), + range(0x267E, 0x2680), + range(0x2692, 0x2698), + range(0x269B, 0x269d), + range(0x26A0, 0x26a2), + range(0x26AA, 0x26ac), + range(0x26B0, 0x26b2), + range(0x26BD, 0x26bf), + range(0x26C4, 0x26c6), + range(0x26D3, 0x26d5), + range(0x26E9, 0x26eb), + range(0x26F0, 0x26f6), + range(0x26F7, 0x26fb), + range(0x2708, 0x270a), + range(0x270A, 0x270c), + range(0x270C, 0x270e), + range(0x2733, 0x2735), + range(0x2753, 0x2756), + range(0x2763, 0x2765), + range(0x2795, 0x2798), + range(0x2934, 0x2936), + range(0x2B05, 0x2b08), + range(0x2B1B, 0x2b1d), + range(0x1F170, 0x1f172), + range(0x1F191, 0x1f19b), + range(0x1F1E6, 0x1f200), + range(0x1F201, 0x1f203), + range(0x1F232, 0x1f23b), + range(0x1F250, 0x1f252), + range(0x1F300, 0x1f321), + range(0x1F324, 0x1f32d), + range(0x1F32D, 0x1f330), + range(0x1F330, 0x1f336), + range(0x1F337, 0x1f37d), + range(0x1F37E, 0x1f380), + range(0x1F380, 0x1f394), + range(0x1F396, 0x1f398), + range(0x1F399, 0x1f39c), + range(0x1F39E, 0x1f3a0), + range(0x1F3A0, 0x1f3c5), + range(0x1F3C6, 0x1f3cb), + range(0x1F3CB, 0x1f3cf), + range(0x1F3CF, 0x1f3d4), + range(0x1F3D4, 0x1f3e0), + range(0x1F3E0, 0x1f3f1), + range(0x1F3F3, 0x1f3f6), + range(0x1F3F8, 0x1f400), + range(0x1F400, 0x1f43f), + range(0x1F442, 0x1f4f8), + range(0x1F4F9, 0x1f4fd), + range(0x1F500, 0x1f53e), + range(0x1F549, 0x1f54b), + range(0x1F54B, 0x1f54f), + range(0x1F550, 0x1f568), + range(0x1F56F, 0x1f571), + range(0x1F573, 0x1f57a), + range(0x1F58A, 0x1f58e), + range(0x1F595, 0x1f597), + range(0x1F5B1, 0x1f5b3), + range(0x1F5C2, 0x1f5c5), + range(0x1F5D1, 0x1f5d4), + range(0x1F5DC, 0x1f5df), + range(0x1F5FB, 0x1f600), + range(0x1F601, 0x1f611), + range(0x1F612, 0x1f615), + range(0x1F61C, 0x1f61f), + range(0x1F620, 0x1f626), + range(0x1F626, 0x1f628), + range(0x1F628, 0x1f62c), + range(0x1F62E, 0x1f630), + range(0x1F630, 0x1f634), + range(0x1F635, 0x1f641), + range(0x1F641, 0x1f643), + range(0x1F643, 0x1f645), + range(0x1F645, 0x1f650), + range(0x1F680, 0x1f6c6), + range(0x1F6CB, 0x1f6d0), + range(0x1F6D1, 0x1f6d3), + range(0x1F6E0, 0x1f6e6), + range(0x1F6EB, 0x1f6ed), + range(0x1F6F4, 0x1f6f7), + range(0x1F6F7, 0x1f6f9), + range(0x1F910, 0x1f919), + range(0x1F919, 0x1f91f), + range(0x1F920, 0x1f928), + range(0x1F928, 0x1f930), + range(0x1F931, 0x1f933), + range(0x1F933, 0x1f93b), + range(0x1F93C, 0x1f93f), + range(0x1F940, 0x1f946), + range(0x1F947, 0x1f94c), + range(0x1F94D, 0x1f950), + range(0x1F950, 0x1f95f), + range(0x1F95F, 0x1f96c), + range(0x1F96C, 0x1f971), + range(0x1F973, 0x1f977), + range(0x1F97C, 0x1f980), + range(0x1F980, 0x1f985), + range(0x1F985, 0x1f992), + range(0x1F992, 0x1f998), + range(0x1F998, 0x1f9a3), + range(0x1F9B0, 0x1f9ba), + range(0x1F9C1, 0x1f9c3), + range(0x1F9D0, 0x1f9e7), + range(0x1F9E7, 0x1fa00), + [0x23, 0x2a, 0xa9, 0xae, 0x203c, 0x2049, 0x2122, 0x2139, 0x2328, 0x23cf, 0x24c2, 0x25b6, 0x25c0, 0x260e, 0x2611, + 0x2618, 0x261d, 0x2620, 0x2626, 0x262a, 0x2640, 0x2642, 0x2663, 0x2668, 0x267b, 0x2699, 0x26c8, 0x26ce, 0x26cf, + 0x26d1, 0x26fd, 0x2702, 0x2705, 0x270f, 0x2712, 0x2714, 0x2716, 0x271d, 0x2721, 0x2728, 0x2744, 0x2747, 0x274c, + 0x274e, 0x2757, 0x27a1, 0x27b0, 0x27bf, 0x2b50, 0x2b55, 0x3030, 0x303d, 0x3297, 0x3299, 0x1f004, 0x1f0cf, 0x1f17e, + 0x1f17f, 0x1f18e, 0x1f21a, 0x1f22f, 0x1f321, 0x1f336, 0x1f37d, 0x1f3c5, 0x1f3f7, 0x1f43f, 0x1f440, 0x1f441, 0x1f4f8, + 0x1f4fd, 0x1f4ff, 0x1f57a, 0x1f587, 0x1f590, 0x1f5a4, 0x1f5a5, 0x1f5a8, 0x1f5bc, 0x1f5e1, 0x1f5e3, 0x1f5e8, 0x1f5ef, + 0x1f5f3, 0x1f5fa, 0x1f600, 0x1f611, 0x1f615, 0x1f616, 0x1f617, 0x1f618, 0x1f619, 0x1f61a, 0x1f61b, 0x1f61f, 0x1f62c, + 0x1f62d, 0x1f634, 0x1f6d0, 0x1f6e9, 0x1f6f0, 0x1f6f3, 0x1f6f9, 0x1f91f, 0x1f930, 0x1f94c, 0x1f97a, 0x1f9c0] + ) + if any(chr(emoji) in body for emoji in emoji_list): + return True + return False + + def _is_bot_pinged(self, values): + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + return (4, odoobot_id) in values.get('partner_ids', []) + + def _is_bot_in_private_channel(self, record): + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + if record._name == 'mail.channel' and record.channel_type == 'chat': + return odoobot_id in record.with_context(active_test=False).channel_partner_ids.ids + return False diff --git a/addons/mail_bot/models/mail_channel.py b/addons/mail_bot/models/mail_channel.py new file mode 100644 index 00000000000..23978eff1e6 --- /dev/null +++ b/addons/mail_bot/models/mail_channel.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, _ + + +class Channel(models.Model): + _inherit = 'mail.channel' + + def _execute_command_help(self, **kwargs): + super(Channel, self)._execute_command_help(**kwargs) + self.env['mail.bot']._apply_logic(self, kwargs, command="help") # kwargs are not usefull but... + + @api.model + def channel_fetch_listeners(self, uuid): + """ Return the id, name and email of partners listening to the given channel """ + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + self._cr.execute(""" + SELECT P.id, P.name, P.email + FROM mail_channel_partner CP + INNER JOIN res_partner P ON CP.partner_id = P.id + INNER JOIN mail_channel C ON CP.channel_id = C.id + WHERE C.uuid = %s OR P.id = %s""", (uuid, odoobot_id,)) + return self._cr.dictfetchall() + + @api.model + def init_odoobot(self): + if not self.env.user.odoobot_initialized: + partner = self.env.user.partner_id + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + channel = self.with_context({"mail_create_nosubscribe": True}).create({ + 'channel_partner_ids': [(4, partner.id), (4, odoobot_id)], + 'public': 'private', + 'channel_type': 'chat', + 'email_send': False, + 'name': 'OdooBot' + }) + message = _("Hello, I'm here to help you discover chat features. Try answering me with an emoji 😊") + channel.message_post(body=message, author_id=odoobot_id, message_type="comment", subtype="mail.mt_comment") + self.env.user.odoobot_initialized = True + return channel diff --git a/addons/mail_bot/models/mail_thread.py b/addons/mail_bot/models/mail_thread.py new file mode 100644 index 00000000000..81e9fa44711 --- /dev/null +++ b/addons/mail_bot/models/mail_thread.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + def _message_post_after_hook(self, message, values, model_description=False, mail_auto_delete=True): + self.env['mail.bot']._apply_logic(self, values) + return super(MailThread, self)._message_post_after_hook(message, values, model_description=model_description, mail_auto_delete=mail_auto_delete) diff --git a/addons/mail_bot/models/res_partner.py b/addons/mail_bot/models/res_partner.py new file mode 100644 index 00000000000..941972a7184 --- /dev/null +++ b/addons/mail_bot/models/res_partner.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import api, models + +class Partner(models.Model): + _inherit = 'res.partner' + + def _compute_im_status(self): + #we asume that mail_bot _compute_im_status will be executed after bus _compute_im_status + super(Partner, self)._compute_im_status() + odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot") + for partner in self: + if partner.id == odoobot_id: + partner.im_status = 'bot' + + @api.model + def get_mention_suggestions(self, search, limit=8): + #add odoobot in mention suggestion when pinging in mail_thread + [users, partners] = super(Partner, self).get_mention_suggestions(search, limit=limit) + if len(partners) + len(users) < limit and "odoobot".startswith(search.lower()): + odoobot = self.env.ref("mail_bot.partner_odoobot") + if not any([elem['id'] == odoobot.id for elem in partners]): + if odoobot: + partners.append({'id': odoobot.id, 'name': odoobot.name, 'email': odoobot.email}) + return [users, partners] + diff --git a/addons/mail_bot/models/res_users.py b/addons/mail_bot/models/res_users.py new file mode 100644 index 00000000000..8c880b11a6e --- /dev/null +++ b/addons/mail_bot/models/res_users.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + +class Users(models.Model): + _inherit = 'res.users' + + odoobot_initialized = fields.Boolean(readonly=True, default=False) diff --git a/addons/mail_bot/static/img/odoobot_chatter.png b/addons/mail_bot/static/img/odoobot_chatter.png new file mode 100644 index 0000000000000000000000000000000000000000..e9147652083125437db323c7ebae8b57f18b86fe GIT binary patch literal 40576 zcmYhi2RPO5|38jYlAY`oLL8LrnVBtnWMuDgjBJYR6+#k1$T~vy%-+X{la75t931=D z{)pK zi;kC&BTdfiZ+cTIhs%YtE5gGA?V;obg{|G}GQzYoyQl>|DiJu;G66=Nya$CJmN z?B!h$k_D(=Ry-!~xP6Zk=Skml0z5p~4!v%qAaPyE{81lM^6Z%`8Gd%#5V1}4Vr$AF?1nV48Cc@Q_d-%9vEbn%2;pU)Iw&4PzImP;ucfwd&+E9)JEG*9`6d_%m zKBjv5GupjzVQ(-eqD2~H_|Yl9E;OI95a0#%$)~VO_C??*sI<5SSz7KT+d49)vJf1@ zqO8RU!oG92)>07AtC9()2&=)pq!@|^TW;medKg3>&Gk?#V@Huwu;RDC3O0hnQrx- z%swURIp>%a-C3g67YFX!TFgI%AqIBo$@dGN>V*~3GXyWON99VZHt~0Lr$&9FcGgQ* zn=?qap25#avi)OoyZWoAW9g9XX8KLpHyD2c+}}kBm=hX}@ML+E&1ucASbZDgHk3wr z8s>I?tlZT^6QnX(peH}jUZ567Rht@wf#CN^%7O>5qbI=cPU@MD((rxIaKO`om z@@m~4F1}{CMCUk)`v2%@pHV6vINey-$C1;DMlUrpy`?TnRp#7fphiHoDgCp59$w(^ z79}$dKX@ddR1ksjJ2pS7#3wcvB6`p1*MmSJJVW z2&N6gap~%=&Hn!V9#ug7g}j&R8dW&t-Z5^vr5xEiB3Kl=8iBSvi_X{QayjUGYCW_x z%$*a&=N%UM7(zKCt$;RC+2790nKW$3g(xaS|B>w&y?73_IpWEwEZH=t_46aIW!++0 z8R4JE*zkMD=X+zK87|>Pme8+ms2ElPyc~4?H$t+=)EPk-8kVCp#)kafuNA-h*BNf5 zUtBflMZ}P~xHG_+*(@HxLeM>VM-txWlG&|$2VeG!K4Q1^yE(#&E^*7M9>T6DXRtDM z7jvfUX+#_bO9^;gTEf)6zJFOmPu0@HYM=nlai6K$V<_> zNmobjzh}6OdD3(%;tQ9l?mH1z2MGU-skM*I0vdb^@Pi0?qHSioS1!@?7>m5AUn+;T zi5}N6awP`~I&qQj&?|V=8v3fA=na7e%lS9p^k3$g(=_ zAV{TlBh%_pu}LxFy*KnD`41C&jpG}ZFFis!!H(ekL*K`i>69j)#W}MOe8`AJb4OlJ zlh4&esll8i9-9%VL1$X+G1lVUlX*~6DG0-26I0_)>E#&wh#p%7ZT2*cr-H)6n+cAT zTj>`bkSqn^fpxefQc8GJ0ij+058B{C$ zuDSVhwuOF9x&#$D_g#hU&A3bQaH0S9h*n;&eksp=5~)LcU23<-o;IH^lVz#gN4*Xr z?tP$&wTU_Zt_d0OSE@rsJu10-fjD_k$MXQ*2gYnyG_D1I#q8^#?94lX4%}B@t#aT2C9&KA;=e zy&OU3kz9B_PB~okQ@4-gyI}2`{C=Cr({GUb
    mh1Waz*Gq7&f@&kVX*n&`3L`}W z_xqgi!+s(gg0MG9^QUjt=KYfW#A)ZVXgqr-Rp`=g$)82;Uf>|AXME3D_7Qd}*kbaB zF@*#EM~o70xYRzZ-UYk5H%a4#IV3rkrl-zDL8ZIOga>J~9BkJ^-h zz7NQY&7;rbG}u&Mbf&*zdFDj=sJSlA8reZLi$%nvc`Cs6j}XI~_|Uzivz4P@lz8M}I{X1TNR2%vcok1M`=F%;3F>Hqdd zt7wK{PY{xW1k1w1KL1udgQLQZ)VORL}YyZ-F zce3|GQA`OYRlUFOoslUf8x$y#y(w3|zUF}>w^vzICX#irFHF1Q%DEk$5F2a1u%@f# zF6a?rSFtHL0bTKn6CJ=QA~G$#Bm0I>m#Fn5_msiHN28S<+b?BgKd@7*vyV81wMRt{H>t4m*P+^Vt( zv#Wh;UU5(B;TT$cDos^%9uh66s?UJ@S;t_be~0*sn+~sDXCu)&x4eXal%;n{q+oJ# zq6D3ZHww9{S=k;5R77gpmb|n57@hyDZ2D`9UU$_dabG0<6Yp%1L7|O8lYbM~8=x=M zrU?|y=Qo)uRSE&T!b>inP$oDiWJ263#r}njXYkaX;~gv%`TE;^9wXix$m$a6GST$# z^1%<(aa&7FNq;;uH&kyT3H!3?(gHrdu8mpq|MrP)6mG<3oLs_}u`883nHzHJoyX^) zC%m8>Hwx(thPqucKQ_HBsegf@8&PHCEnW=#yN59%>>9{uH`QrqzCQ=T}3dv&7a@w~_e_%1o!cc+445Jjr-2 zGfUdY3scCC)^GLPw~do`O*7DId|vzKLaQ+Kr|HT&xeQ{1>&`dL8w^;S&y2Q4i&zN# z>f~9oinL1#5TA(RSGtyv2^?~F@MLj@QulEj*ic-zlal8|WD6#`g&`?3lp@j%VJw7P zCdYW$=7^H=507}M9EDyUw?&ydzA)#u$`USe8!P{%EqJrc)-sDEK{eb)SY`3{zgiB1 zA(ox4rQe`;7_`!o_`^!*BSdme)mhZ?=pXC^ljMYi`H5?m!2p06ch*R0q%4eJjT2dw zW1DsBr%9lyJRfxa$Q#Fej&vti>jsm3XL-$hsn6mHx5VXBD!-Is*?8v@*Ysj48nKq7 z!YnU_kRzI&6TGw@RwvVy{+d2VA(Ic(gblF1dvGSyT>Hgt;_O-W-uuDA$5|YSu3)46 zQPTM7^?8g@J~)&dvR>JJ)1JiO+!OdF`s8?GlKi5ZSxORVrr*y{YS-_if3=-}pllW*zVf*BQ}t`Rc70 z4M)1e89ckir9+~qfJ8Oc)a8y6dDF{*o2QXy14Qq-VAZ8AMho7niHwrL@7jXdD7p1@ zN(X13BOHaie|?M`Cy)>`VVypbrrFB7YoIZKR*xnP1IyfXq7m|jLbu9<1Jv~3*1(%L zUzL}>o)x`?sQJHg-?^#Y6RR zD6dQmn`L%ZG4v*G7i+_G%^pQ%HfMLr{Y%+pxUHSuTuBvMtuW@_`hBkJNQz%DD3&1v z5yJMsI|1~C2$szOo+W1%XA7a0v>f{0d&hG#`)wFDLe_jp9dRmi?|6up?1$-gI#!l< zyehKKQWQSFMjKEnlHCkKxKk4~?cbC{xc1S%Q<5flS(4A?dhGNS|D=fU-ZuN3!DE;iqYU#}E6-I_1QoHS?#Zl^=oYSBc@ z%*L~RP$u~9&g@uRiQ5jhzkNuH?Ympk*q|^L0Ijmx0uWWV>n!sf=&C6+JMRV_sh(AY zjZvMnX_1XFQ8=^*5R7ei5af@+7P?<&Gfw-rp?$WD9dy4QZOCm!hS9t(6)w+^P--tj zPGvgxW#1&PxnK7}{N8jBe~YmYlRTxOx|BC_@tHu&abw1#5$XodRg_0j<66Nt!s?<| z>r2B5yiD?BH`V8!B{(h}`+Ps~3;1fLH|A#VR#iXyf6(LRz5FV2o9%x* zze0slfuY`)c7DVMp>#!_3c;7+q3b+-81GAAjufE42VA25^UJ%u_&>Lc8t=jMu(mj4gBP-YwCyQ7yj(&N!CN-nW&D_}DxO82LYygMmU3x6kMMT*AV) z4gF`oCw*=0yF|vAIPb9b!&^^OzK+uMV9RXxSza2G8+PA^Dw>ib&VYK8~(!ZjUEYp7cBpqEnF(Fp}BYM4QPC>1ZqR!P)`2rMH6tmgA@_Zgis{m<@+qeSEcRYp&&Qmc zqH%{8m55!*B%P9XYyQ9MMmjt3nqa=D5(FAw_F0LZ9Kwg8FHetqsUFj6Uda~A%Erb9 z;pGL=$zedYUCo_mjiz?~{VPc-hq_aAHAL-;_=GMs8vYi7EA=GzAp zV*}qgE!Rgx7S7GSoWTmVMQ6+PI>2<}RF%>x?iBW_mX_h+VWK+M0<^d+*_exP^8zm~?}J#@1f?7L`k9%T zmTqn&3)q1bPqWG}TjQ@fk4zCOr-x`kx&#=s9ulWo7=~`@wt5oV!&>BMMRThO#x^9d zzrQ~`GP2af7iswlDZJvD$wKg0BUP#3=dWLSSUQAHGw~f{ol2q3FdS};6rORRGER4_ zH6aLlCY{^2FS)w9`reinSyy6EMx)daRE=)ZZP-5Wd;S_TG%NkPv>hF!nUP7?g$CmUxWh!>a~NDCb4N=pT#r4!fu*;5B#KQRt92CLy?1}5#K za^#33N~o(A_;DZ#jmb86#z~==A}S`@{kG0Bt~OzRJx_8WH)qwZb`X7=MCS>ybgR1z zjO)Q2Ja;C^{vNAjcP)(x>bN|luwvIU3I&5t#9xn2`+2+mCGy{v{a$(>{Esb$^^W3C za{OLhN1amDk!r3VCIFT3=LV(R$b=vUZEdt|3TKF@sqsFIa17XZ&S~_bpx5oR54Gf` zQ5~Pp6`z<0_VBJBgGx$+-!5xDmw>Qvq$?8aZ=DiKz}Ot=KpAyCZb~3jN%@CHgS*Eu zPN$`zc{V913AeVg3NxwbJwzi0>}tU3gMIu$KTUyER+8y9cmX&O*yP~v6fyWP^lKD2 ztGc?nccTRH4Q7jL38cD>0@_L+|McPUauN@zF^ z(nqCqsavuS+7wu`pxI>Y%egOvymzjrV&apy;@{AwRn3@-5;e1~)i+L$$mdem(n=k$ zX}X8Q*eAQ;LW@itD00kq{L>^TzUCb;^r;B6?a9f>IrCaqJesH zzS($e1bB(4^4Nts*D#iUnl#=>FJ>EK4gx$ETR?tdeCV*bO< z?a-auKb3f+k&Z>?oMnzSzLrNP29qs_Kf_<}qO)sxm5_R3TK^pQ=MCF{U;$;iGjf}AtIgC1;} zw-D~gMlpMzk|PR6rpf|ov9BxV?w(Nm2Ya?X51q;oFfYu)Db>ymd|XXNX?0WWZ}c2z zXJ;Sz(u|#ciHU#Uq))PJUPhwNupWk1rlPn*6 z@m28t+-v0$Zkg;{HtM)kJxZr7+khyF7>Oci{U0|TbjYLSra%C>A5jwi-9JHA`xrpg zT?$&J&_3YVCM$z+b0_T0!7X`P-$8yegrjg45h-b96KuvMzjULhGGxAjsc4M`Tz6n{ znJJ$y*81&tCT;!u+LQnv*a*;mMgATDwUM5Y;lJIuCTzR;Z*OHNI={cly031S)7?*A z_6&}Fe?I~aTB~|Vw*yBUBPBh1ncdCDFuUE#vw#>=!-!>|R z516D)hWoqe4YssDUI=avEz+}+%(AXCShFhW=Vz2s>Z%@acmmTO`~gYrx(G%KypmWl zPp@6d?yi5i6pg-w)|&9T&9}sqmQoVl^ft5i@E`-zO^}X=ghZ!o3N+u9728B6saRKJ z*^vnsB_SB@l1zO*W(<9x8G}v_UIB_9bY;*5^tcskX1Uh!ZTxAw1yl0;g* zA6fy&)+y;eJ{G-u_b!;JiOe#I>?-sEqa!2G?O8GNpk2j)TH{XuW3}v$O8DjU4Wx2n z{6c_qDLiZZFb~WBu9W7M`C^S;v51QN_(2(ukiu!k2xM&ox*0K6EcN_eI&%cK^S#5Z|Q8{WbWcQ*CsQ z70@glKbbbaPOz&Pn4LBCm5J0T!S9%cyLeRq6hAJ*{pG!E4VUu}L zsjA;_A(Z(|spXg_LqivLV751jZ4E?({@fuXr_CrH&MXtN()b(pTvOlJMVMBzh(?ut zoH45bQlL!1Hb8m!MRu}IWvipMF)jVi+~;;AgdDN$g=^Lobbbt_RFu{Juub}7Qa-WA zN;FYkAanDm$hwLqg?-$q_PuQ;n8}oi4^jsfMM+BZN}I)WC7JBKy(uRJ?}T}zXiDd< zWrUt&G`qDTt6run3TN4ZG0sAOb^N!<2;ML1`IlhI>3VVjdeUf0qD`r-pJs91W3f38 zpIb$k2Pc9)R-4f{&DT9mJ5NtX$4cNvjCK7my3`c~>Uk)Ge}6zL#8bC75oXrh3$`0v z`fM}(WV*HZDy_!8(PN8_J(UGV7Ux=P5QWi)9NpLfj_+-0mN51SpDnaTjzmb9Md;-T zAH<_?_-+`E$c}b*LZ1W>smbsaEZ4R(`-urt0IXFzX4QU?J^BK_GxjD>t5g38eQ>8=$6-g}MK!?>;u@BUHn)O*bsx#Y zSiTTsX?eU42zcBIb>nQAL~r{X%NAcXc(|@TZ@lNjfx)47yZk|G=!DRir{Qi%Z6-5$` z&Fiyjux3XgCDXuMv>Pm#VgG-_>8!8T?dUR=HyDFwW6OKTnU z;k+ijt$&-S1IA8#M0Q=Goa_CVy+agDllU_>{pW8~K)`XlF)RK|vSj6-wgN>qTDX6_ z+?}fkurO;vtm6c(wy`Pxf&Hz$dk5$N>*J~DCVbg3vrDh~3zp^y# zGbc5*f<%m`SG_nBMwaIXz5Mlt5L-09=+Do2+h6l+SA52tT>=Jo{tHo@#wNKI3KY_5>;n;sA}UFaWR!@U(Q>B~g+z)th}#t~Av-#v86kz?{FJ#fuzuxG3M-HcFI_ zWg+rugw=x5z}^XarT;lQFnGN% zgACttZr69!?qm=bRd_GMW)#Broj<$ChVFbG=?>+}D&VI=6?78bP2k99 zd9R~ig!U0A9ef^vXRKjeZPG)EcivHK_3^7-8Z&(4Nsgv&t@_6FhLo}}j|Gm5@kbO= zH&s$XveUG6)TW|WSzthmdNoEx;Ev*wYDu@J8fUKZ-p@UOe3qT?8QMsSJF)4Ca_Qul z7t=T#W%`%(*wMYqK?)f&qs7{oJK(x@&fG8F2hTvj+fi1@%p)VaT{E=i42%Vi$d9l% zuGsTS33qKFjl_dP-7%lb(?3E-wgQkkqSHU}<07T^PpzRa!$F8}mMM2y15gD-g{{{G zdko@8(=!^3AoB5XN7vbrlO#NZ!Ez#)yK}1>`ioJmL9&!pS>mNd6!^G6 zPme8PEhBtNeeLwfk{RWg+boRVnbthtqLD%PXa{glftds}5H&Tm_=JQiK)3*}&Qy;C zyqASwcy!cr!sk5c7Nou#Y4hy>0IIZ|nhNC?DViWp?nizm=+Tr~TQv1XWO7 zu7B?gFcUkwNT=Gt%}oeg=1LL}JTMD=MN}yilfj$_V_A%nIe*Xu+Xu$jYkE@OfW&I9 zy_l*e%hjD%=P2ggxh#?6gSX+sztBPUPANeGJtIyLKT7xJ_b9GLuXZ> z^O32c^V?cdU4=Tya7MAtdd}kGgUgH07RJ~5R{QI7_BF$Fe_zMs_E!J!JcTfs%h@$; z;mns~LoXi_&DM<{*Jtog{BUV|y+WZj!r{&!*-f34yJ(Spg)@0bqgKe3bxTF@=TVTf zk?|^gJ~-oQeJwQ4S=r67?Q*0riB^^4m^t)F{}Ra@l+>R4$x1SNIbs(|m)>&~d?|T0 z;b?F`6)NG+C_`}kbCbxNY=@MT(W-E2+1ZfKmtLBfA`dJ))oanHZFzT@$8$vcW1CEr?}iVPWvk1%HkCc$N(A1uV1PIC51 zUrOP=sD~6e3#mqq&wt|^kdXbHyoI(mcHg5m&A zcuc0nLdcpc<^8eL_K8_pN?Z^5P24UYKMt8;>KxHQ2kxGvwKbY}PUSLg{mH*dg<%|O zpI@3-6`A*WOTi4eyYdU%G!85YQh2 zk%^oU(*22J8h7}JP7Oq$v={6xXpM<%I{U4AF&aZJDig7!JKA|2?QMRzbI|pq9vZE} z%!;YwA!ox9V~#q^12w#>%K=@mSs_AM@f@j?CcG5Ya!QlO2x@*$H~*Ng`QN{Z4D?#r z=p`h@-83)}NATE|7OCjyA7@rU?KXrGj}EeW(NvL>*7}9cyFqvzM^c{>Ti0vn?w>GyI9ryD1R5Y3y0j7RQ#qGB zgbf1bHZ~|;DQ@@~RUdSWhZuPDE?u2UT>G!@_h7{WgB?j>*N5d+O7r2YpuGQsTFNeFnBU@@ zJ#K`9Z1Gguo{HKJ)$u?z^6ge2rFxwGt=s)hRmk+(#I}GF3j4J+zx|zi=d*e8zaVkf ztrkJbBu9R;?Ws^tac}R7hnZhXzyEf~9}oa-3UwkDa=Bv(#zKn0eN7S~%LWqaA=~-S zqR)B+j|a{r`4bbNc}CCfE0WlGnL_USyI$e*D4mIM6HeBOWBB>CpAcWhQJX7lJbFrc ziv3ML?KJl(e21uS$+NNh>B-Valy$zXADd#pLGAQEc_ZBD zi3)f=%W@;19CB{!421AlSQZ=kaLWRT8M|Pm8%?oSvi+`F4k;@MBBXP~hjbTbkNWmM z!VLVaUcEA0_6p7&aEg#`Y}027f39KM)o~XuvhH7rAA@(Onj7#Cj^goUS$5oOPoyLr zu)5%84MMNmvf?gg6K0P#F@Yl!^jBRQREZ5t?tC+K<&U5ItNV+g+@=g% z5?(uUl7KE~i2f&;B)|^?aIamOjm6*kHGZYBNQ6e=i=- zdAL$xw>rx>_9{GY95A)Z`$??L!vP$>)_IwHVF`&96+ey$5luTxUPIfp2W+sBu7h{s zi_^>c^t9+^m11A%j`X=RXI$~h*8hyH*7qG7gH9;v@$l4VqPOKJdx(9v;%3jt5nDC^ z!dIAIFV9z#MMSSIqgn#DA(d^X-174B4xf5|{i3uV{k&9@djT1+q0pFY8d43+={wn9 z%l!E9SqeL#Y;rq(w1K*PLLnvwjN;ONV`_ftl?={R$Xn0SvQJ|LJHIY7TvF7q_!5k^^Oy z|0rM0#K8CUwpnrq7yc}5N37-A15#@vpi(e?t&O=Xv+SZnS%$>FNwI3An=Q;l2-tIdQ z+rDL3c|?JD-IX=hH|GQp8BljZL$A^&Tg;9Vr7q_Ced*>CM41%RL4^dkG`^bK1v(I) zMVorQef9wh!FNbpac;<`s;Vkv@0K4)?AJ{a)F)M+%D?M5ey^Lmg@lB9fRA&A#jK6x z$ zM^SrBu%dZ{w?o~M$~c7<&D?WnD!I^gXbS0LP3>n zhX{ZP>Dzv0)^^;(zjuUmzjiP3xq?=BES72!=4@1PfVhN*hXWb@_wV1_@FAE}27H*rf`A?fhUwXNIRbYLKIr~^N03-t#y88Qf0Al7`J;GUD6*j4hCl}3* zaS6u~5C|r4pl+asGgirOCZ|tXl~i{pkF#yk&-xC&I$hb5tf|_<|ZYGy-4L z@4n#i#DKO55amLnuqQ_Txb!nuEKto@36>Pv=e-|lua<;sJ-9F|C)i%w zj^*i_t=~4O#wk_o;WIzYthbfP-rYWpXR=-W<4d%`BROhsDYVh#_92tT|lAF@kd4SiY znu@<>X1w@S7O65=-ldjZFW=!ej|u@0&Sp`aD6Pd_A9fv@%7Fj7Zcb>&7d5}&=-2GF zbUiOs{oGHduVtQf+zm}wu;2M?Cs2ca)}oa{c~b9eubw}D>)f2m|G&7zl_^rA{T@l! zt1iE~q0{dxh=PvVquJKZ+Nu5edx$wK!uuTr;R&K||87wwbS}+7F%EyWKCM+DD_-M%@Co4i1Ra z)P{h03&uc$V?gMc@4|9r+hu~rhQ%(d zgq{@NzpShb4gXl=`my(Fzc@6Kl?Tu<792pLCESee zUJO3SqJV?2t5}7ETxuxl}~CcrplcCbRf*;gSz7btagIviii)hS@*ghwq-s_$7oM z!LJPM)>i#7vuB$N%x?5STYr2$CES{EiL4CXsyseM#R>XkQ_Jk)MaYp4RUe;Pgr;VM ztfoq0XP+9Sk4!luzN^QDp738qucY1goPjV~9MjA_HZn4`aq+{~*N^T&b>D*;q~B$y zfN~TU&9sLyuj|F45$@I1I*ahDISY@|gGZVM~N=y$x!>Tc<3djt(S+ty}OT^ zANc;G{T?~MH>lEE4O}>rJ`4!Yun5UaPPQ~fSo?`;36OJU4UPZG5`}d|B=HLfi+Ac4 z19({Um^W@bncNh}lC?30T^(H3ff_)jPnH@l}fOV`T z+B=c;Hd|XS&fRv4d0C>T%#lh&OgxUr=<=C^{Y>aXfi=Xrw%;iuK%hrYUy%$SG6di* zfSLdKMv1OS28q{1@O@wf1xdq!+}egg`7@R1#kvTqL8z>4ZLi;_p#8P7k`x&Hz^R8o zAS|)0F$z{`veb&n;kQXWQN6_MD)~kbkPuLUP$50V0*nS*cUJgA#r`pD#@8IQ6}R6* zApY!Jc#^t_F#PAeqK1GG2h0X8LPnp_-C$5=Pns#;-P5YKW)fr=fTuhjnnku>5 zP0r9~>n9qzU?ATzcj0@MX!Gkmhc+W7`I}N!JcTHWvXN z5RmFzZ@gD#9}{$)5_3)nd2Vq!kpVU_z`#AU__zdk`7DpGCNX5WS<4rs{L3O4B}Ym7 zGogD6sTvP=gQ;cTlf84u{LuQf@2Bw1pJtFzuZvS3JA3=L31+l8Yub=>BT#fgY(AAX zrIsH-=Ig0Q`?ErwnZx7aq9T8Eh3{*r`M^i;eV~VfsF5yr8YV~Z)f@(++nxKK4(|w|%csi1SuT=cbZLL912Pc5R(dU^|-`q8V0YUawX; z+7g&fDt-JuCgu)9U+-ge^T0!UTE@Bg>CBg6ZTBmDe1k43+ux!@!4d%~BO}=DTzRo6 zPCcagkcV)$yEpI+M({G*bj^R>we?ABpUnA=q|C`WEe8h&K-c>EC!6rrn4${o`R4S~ zKf}be*+0awe$B^Bw|#xOYiqw>t2FT_Q5?WN5KgX=^%B%9_q8n9Prq3lljs)SLJ_^- z7U{QHCQw$Ms6Z2SfqepS=8Q{?uUSmX$T;V{XW?#sxDj&2 zREV$$z3@d)t(qhBE71$p5HNEA3yc354H`RKZOTiu+n=sV2ssBKVuJSr zHZ=6~W{0h|#&hR~lB8Xao6B?7K;jL2?9G$ss0g6~fIQIEb5Q=$`CPz2ZNvB`oS*D7 z?@qtOc{22_|^4j{VRxGfs-)jDR@(0Wk6SUwESxF2mQ_GL%ha#7^T1&vPia?r4 zxo2OdGYhJ9*NR`~QX=aRwF@t)PumuFy|PjKtp_^TKg5&doeR~j{s#$?Ka)bvg^MkM z!{Jv23q=h&-LiMPI z7=GSe5s}K8-!3{e>Kx8OpAx(B=f7If0r7P9o5Arb|I7%xRNz(Dui90aCItx$SeQ4f zn&_U49|be*3|0=H#uAQ(hGRZ;-U2HuDl zFjO`U#YNm^S*YTmtHZQFIRUXY35hsMn%M^EjY&acBcl#%FkwXJ6?{JM)Ho=MzgXIFT#*yBWM`(GXq`!7 ztc^h$wW4CHO&ZH2oR$Q3s3FWWZ`|R%YT>`lO?HYHvKR%0uPcB=gMs$*r|cJ6Vp`{M zR_x}?$O?}kxWnAGlc(G(gmY4Jk^#%)nmOVl31|_Ix13C&@ojD9AxHZbujn|YOcKDn z$gnt}$W;Hd8*^tq5W}9res9btC-l;oHJLyrXv+ue@HsOx$;;z{Aiq^e2EWBmCSG|B~fdR6a`tCb|d@#qo!Lhug3-&PB}^^bRe~ zAnNhv;_L|6X^!dX88Hg1@d;*Yci}Qa9nWO!;n{1w3DBnI8p(?T-C2;7K3FFO@l_jv zfql4?8W&~D$>VQypQc-GeJa{ba1Tn5IY(V77n(ezA=n9LZc!~XQPkB3izFtHDF{Eo zCj7rFK#}9#DZNaQ9`}u%gGsINnQ@?-V6j2Fas1?QN?=c>{;uoM)sh-0t8CTeqO`KK zeCr|F-M{k~(F>~qQWlmLU)%RQc+QX3{XK$jrB-PGwl{uwqxz^mH$9Bgo7B_qn5(HQ7fKSEz8AFwES%E zID??JUYon(ef+zzaJsymKPXk58p6*obbM0MQwa$*?zGOwM1ZbO6so*4MVlBwbtc_jias^7s&X>>Jt@<03+?@$g$ISG8QNM((P!OK-hwR=3$;RShkZwAestoA?1Otd9FSrz{jJlx=05~Y>7UbpS>1XUvY;A1; zK?X=?!yq3-5Y_dgzCKl*BQw*n$-1w%_ua&5kNv^n;W&s<0purhbpm&Eb_Pq8S`REh z&=vmvWd~^~FxHrv?Lqo-X~`1Eq~PZBU%VGNyRFPl@ps=pdA@1;8e?*T$#Lj@IDNvqM(=fjVgPv^S!eMzA} z^Xu)uY5c*-AfZ}0A3|-Mp85GBD{^XJ_jw|ajGuoNzmNtB4=X2=ST9gzZ$-#yd+1W$ zG|j+-UPZO+_G%r6%bZ(VRna6#dVaR5>b`!>ZBnMi>#D4|7N=fk-(^k!Epo^(^n%Bx z#tdXt@2fZ|1+9AdS787X&C`dK_Yw||z?VdYlcY|$8ch=s;225LM+6q<%RrSGZN83! z3(-ONc)gCtWHmpFf&#h1Ds+;!T^?3?x^9YtdIvxdjA8fvSugm`xfgtQHA@C)-rBGa zAfPVkdCt|B#JrNgd_`%T4zh4G5|NR92jlm^*ub`3-8elvJD!#~=NMOh7j!vobPHn{ zW%o%WM>r~l6W`h!r7v*pe7t#mpG2h<$9JLw|?8vl!ZNFlwA7M;e$zq5?^j# zl`BqcAw`7M9Y9J{D0XQqLRQp^1(JxhiymwzX%vPDjm za`{js1Z3BN?mYp*{m$)2wxIEdoy@B!DMbPSay1$C+|eCA>&ch(#CL9cw%!^N*6da@ zzjnB@@6@9dgkZLq^p?YA?18>u&stFheP&mfkfg*rtWF)c_=rt7ltD>J>AGfugM)#D zC@vwf{_h_PQ_|$rR0IqSDnR#TZGNHr0~dsCXrKhAy}iGEKRqeLHDvH7%> z%z0@NJMrHO^P11}Rc`%%RDF3k)ZHJpO0p$ek?bKMWY12BvX;rd?>m)!OG5T2dk7(9 z&%T9FO-N!ec1jG!K6dYCdVcTqUhj4FM^6tk^F4FUXSqN3eVCO>dXCRsw;EsP{PPM+6X32><~kTiYU@DTHFSy5F{X zYikR#Q^W-puIzp?vU{@GbV6`~BOoXJN9Tvb1|4p#3r(cbNQ=KX@X*nP@*&Sf z;7o&>0EVodnSlHY(p&rZ=zqX0FVo&MZ}P@odZ?VNe`8}K)IdrI1Z4ImuNCTr zBz3MxmAwC0+e*@3j{;W3GYhj}qK-(qBJNsY)kBfPNk@7XI`fQ+?+gR1RE1*0IS)^? zWY;C&m7%u;Bpv<&uL$*iC11v?Ld^#yLnINmpWfqOuvqA`=$H{S+wFLKSSWWu=8U#5 zf18az){h@=(nfJgN=lwm<)T@htfA3Bq@}*ewMg$ZD7B!Z2n#zY^PMH29(ATOG0A*> z)dC{RP4ZC7N~Qe!HhzBL>+2+^{horb;`2N$KvJ9xu|k4Bw2aWbhyC)%7&cmRb|fg! zX~+rn>IUd9FLF|oJl)ERC1MJkIzFCkJIQImK))p-f=+Prpu5RIWavd2e^k~n-0R=mH`kpdH+=QmF# z6H`pgvb>Hi5z+ovx+p+G$}rCrbBxB%TnKI0x0spT+_x_HQM!RZoE2s6b{(;{%EX^- zj<%|fAKKXP1I)5>P$%eEDtnlL3bC}k&tO)!fT?4QxXr|*eleP{-*z;kPw)YM4w5v@ zJ+)cq9P4G`+1W>LTbK~Z24zDp>={lWey6nt?az@A0bi-0UG7#u>WhNvkBu6th)S0X zb3s*B%IUS4PUx~4o0^n8GW}ZOYz#W|O!Wh`uEY`n%)dO|K-1#^HLX&f4sd+h&8v&e zjEt^yc6Q>i3ybUYjEuC3yPqRzo_AV61C?HoF>KxL(b4(Pi~ieK4xO6?dK*7*46y zReRG%d)X(PsIu6EUa=Bwnf1?B?~AztR-rg#TUji)pn}lVsT(!vZMb;gWiqUFiDR)q zZpzc7V}`O{dXu)yUsz^iw(DN1?PgACxhuMY!M-uvMT)!lZl^P4e9vwNU6#><PP z8k3}{a0^!6DtiB4&ghxH37yMRbtwQ}Za_M#g9_Jbd7|2A?FrMJHv*r&F9Q)f%(*uD z6*e>XVF^uqPbE`ExI^x_t)XH=&-IM1E=~~*r!2d7m!IUR3jJAvF_E4z8QMcTgYCA) zWJy%WkbQ*{&XxIA5baBC+BEp_@8N1Ku9T7QN0P&8_dlc-_}=@LBvIHO*)X=?c~X%! zcim99_T4qKrdI8dRh`L6yB_WDx0D~V{d0e5FuAkZmr^J$u6|YRKosR+I1WHScY}NdJZ+kdZ-6$-7ZE!Si@oX23%9(FPuIE#%gDqQ6p1ig`u%9|ok@`{aha^Dl=tfV za>Hix=BoeJ_D&*l;g;XLV6u%)(Zi>EKp)BgxX*Cl&g&$4lYuNxXUq zHfaKF8_J3~l)yZtfzsht@XXb4Z}P|k>+4|(uEsDnVTS2rP5uidcH`zJY3h<1 zC-QQcnN07ao|T>byV?gcSD;ruF}l)N2C5NCz4Ex5FhLSwFxbA@z%fe#Wt6)KopRzL z76WE8d`R{r^`8R+@TIck0o)CUBv0DA>gqE+m1=c0318oNOl17YS7MK|LeFoW4G$+$ zD@4~n+Gd!ep@G_z9?E6?#~RSYX!9lZ(}9u{-+faoyQSB|#;kPCz}NN5f2Sb^-bv)} z^Y?$r8Of_xJ}{g-+hCWifb!h;^RM@FA?JWBUD))mEpyc0+Oj4gJUx=E!8O<%T!vxi zC6$?&c;fAy$IU}0V3PZ=bP$EQC(YdHy8p?;}8cNR1%ggNR(*%6x@3OsFUYDFb zO=aO{S#sk5Q==1NN!KWIB&dfv67Ys?}7CS~)5f+Tr|EB*TlGIez zv?Mw4Cd2Y-eOYvY19y}nKyJx4A{sOUE1!xF^x6f}`Snde@Ef7hxv$i-(>E%GMNb^8 z_%Jv7|MUx33S1Z>lA}Gx_^Lsen9YL+WDPNX)3KO!yryO7gPpLD%lB z+94O&uc21|X7_b#XujGF_wn9yt|cfN-TD|*a$Onn z_Xbo=uzgI$(=XKwkuZ!Ox=sNYg_mwNUlv5|uk1wi`+SSzsJUeJST8BIpbKTRP}6pv zC=`T9Gw5^ghd_}8rDpo@BuJ2Gl^gQqV=fWhp~ z&@FiUGK-S69m2Xsh7W&Boe=fSM|vd)o#*_F6w$bCW1#~Tal~zPb#FI}FMb*!VWuaBA{0+?9}gCh7ojFpl#yI)vb9OlJO zyu9$A-!yQ=6C#O3&@y-ML0xc!1;ZPkd8(Mk_I%!ywDdQByj%A&nzw#NAEJYC*ai=) zXFGA^DlOOR)5o`M{Wd<+0#N5lbQ9*`ck=R<882IScqC*tsq6|Iugje_53o#38#OrI zJvpG^H)&Ld`Y@ur6?9-UA|HqVVtj`llknUZNp^Ylq9;WdMZ9uO{ zdC5hD!|~c`W@g)kzDz)UK%w0ZuZ_|#Wma)6mgt{|Gt#&L1LG}rw}o}NIOId$3Enta zf$Wri%*;#;v-kcBak~i(Z?jH8iCX`2w?kV#_6S2jr@A1i9vgeA_jScXnkOx$cJ}r! zR5?1neTz!H-fel)Vn>VXl@kM@ukUoo5N%6NjskEDR3%>(7?vig72fjRk;Z3(>b%gY zqY1b5RsX_WWZQnm5Pyi!@B3+)Qg?iIMhh2L`+%GGoI~xy2fpaZE3h)5Kxz0JIrq1* z@AEE)uy8eSi-44+ddDAd^8-`iq+XXxPEs(rf$xGoy#41lA*nkm5>M7x;Ya_b*QVdk zge9O-oS|G+<79Y(O_I~zet-u1xDhzUo>kBFCvZ%r3ARBNy>~H6!Y&>wRe}HKQe*_ z9snNr3gLwS7zNImefmyi8rg+2VkU1ET)WYg@%`*{u*^unRNM{ZT)R||Ig6w}L^O6I z9#nFiVaj}SD>!T8U?J5Yi;Z#k?JxD2B>*cYf3X00z{Z8E-G9vPK=S_K{?97&5g8$=2~1{=_UUG(aauZsrH6Vx*xhAqSMsUihpkrjISB9 zkDoG0h0C8dPO-WZzk%GTqi^wvoI2`n3$sY2jXJB?PT8*g@@_*%U*=Vqc5`uE8*OpA zcCsRO;HH|Nh>v5=3)eB{s{zxWWB~(c@%J7?qi;9dZx>OQlpMRv zJgMMbbGQN>Ip7?Q56h#KA&Np@%1sL$OQfYrk?!5yF;R`H)R90fP_NXt2AyiBpdfju zK3_N=a(SXDMVlM;0!-~v_4&ApDON`+VeA1aRK>`(4`(N9a8XA-{h@kdBOZ*PRbBTv zjQcjz`SjfC;8e_|6V~a?4d1vP{vt8q50O~KoTnV3rDO#Tq}A0u`KIMm zsz2Z?~TzS{gV$ zG8kmYlDjg7n|QPiYk-fxQrD?6boe=nu5-{DaCvSy{0UmKOU>s=Da*{md>&Ms4vmc_ z6FJ*FCy~qHla ziS}#~+Db4M81qMCW<0ieoF~sq_g!h9yP=jSQ3l1TYCp}FPd89MLQ{%~os6~{%Xo#T zhgmvrI|;t^mKK=)S;d$5b)F%A+9KR-GFY|y!v5~RIOMcae~|^p)^?qdYSAZhRe^uz z$3(%}AZ|lFu6O-BS^;4#-&Nzba~kshh9gBV-z8vOh%Q<)JEL1|q|_QOC#*J>RcQKH zp=;s90$^j2wX^RD!)=Fj?kINB&W~9o*OpCCCa;SB9IaLpXw|zRwC^Q;RADnJr8K?I_oO(x7Yc^V7kUcnBw1&^>p*`edZd-jHA(#uIw(y9ASHhGp01@dJ^7eP|NL2 znwb|i`mVDCW`wV>&&^6q*NY|2=VDF)3!9GL*xbfOIW-M5bN7pgPkd?&yJ}tfvSfI3 z2RMsv4VM3-Jj_I!8XMEP{OHe=>H!*U(p_a;38VGs8TU#Pz6MjeQka56BD9lm~QS>9OvRp zQPIl30~}zQak*|*lk^!dzPu*iXAshc<>wnbx*x;RD)a5A6-j&=3=XD~S?26^v%jrl{nFU>X7H0^1Yz z4|MP|kmH1b_e0cof~`drRQ2tLG-52 zg@Jvs7rn#9HAaRCWj&#S=+cO}#xFMxj8Aa3GpC(Dl{=>^iD-N{sJE&wYf2wxcZo8} zA7Fh~Y`JGQdN{@q=zp!RV*SpjxZXjt?Dg{4+C2&G;%Dr)TTE@tE3!i)F5m%fziK8` zp?V1?!l1kX=QFEmeQhv}fEwspx}O>h)q#4b>F+O<$blaPhVJe8J~6p7C^ArC1AZ&H z@)|J61JG2rc$AheyB~(pFk-1%n`Z9m3FRoEfbBs*?LLOi_d}0?Eq@awh8zi>UAr_IvXfBF;f`JEQdUA=cu>55Js@ z^^NC!^e%PQsu7;1)d;m_{c`7CG<|KayUxO*=8H{Xs)7-Y&w>46q=!u~M;izY$ z_5`a8Ay_6@RVwAxv%@t7n1(Xm+|8>A#I-Kbzr=eylq8Lu?#N@sbF@{Tc#uVO&dO}$ ze$UYGa3wnSZ5t3d-zn(oKs4VdPWJ0Vyf1#I7ufBaZic{drcyBEy9C8EV60`PEj)hp z*QiOLn*HH1bLs;W5k3>^$CuE>ClJ3UPMeE1i*5l|< zs>H96iQz0fzZPyggiI7^nPZsm%+M^S$4NtxuuPoGL&{klVu7BImAp5^!=^FUG0Xlt zT#bviz*baZWI(|j!KV6QUNc|oKsRW3bL@6D|0nA zAHRUMwDRHRjN7^LdnN$Wd79*WN-wznyb{xx`!mCWp1rMAGvTyudT?CBVztj zT|rHa>w@R`=e9NS|3t@%GwaW2&v#N@f0}i%?}23Vly89C`68!mJ!i?opMvBt!GMxi z1)Ktyz6z34@y%Y8gF%Zf52xQyXLq_eW+S#6eG280j=a&6BYx@XMRN07{GMobDq_m} zqI^9H53F?!X79S5)ks*3QQ~GGQa~ubx_BBWskGgX7#ZJMGrW5kZT{{y8$)exTA1%c zM^BzuqU06r8iw~(>RjXFu4Tprwg#}*zdyWdY-qq$fO#&G^rB$Eoe|`|#>RhmFYeEJ zSQ@^X(&xWc*6U?f-a8(B;K3r}q|;(b$0FO~F}#J@vE3-cL?z}xEy17CMQ&ToD0iJX z8S{IyQM_4f%$9e}cIZkwO)j?WM3zp#bbhug8c(G$L4}DT1<3)S2ihyl%gORTc`(u{ z=V)_lbJGsTP0?xdqMFKD4|KR&(q=1?rrQy=Hql z%#l}6kZH4-=|xG{*am3D9M(s>GGrm~5j0TWumEiV>a#fsh@#Kh-wJk#F`UPncwoeM z>A;FbWjd9}e9|D{uc} z2m}lnVQiRc(&RxM%A^Z?xo{y5IhGt?t?|2@|%y^gxp+l*8z7b0&FmnBdg! zKPwJ;;;o2m(ajQwhkma94aib`fnP>BrA=|TC~W51+icHl)EOac@ad7-Sci0&6Tz$! zcR zAW7=530Lm`m*S1IZ(`n^`1|Fkgm)25=<$V%-kF7MX_u3{=UO~4`@_Gl#A1_@vHvR{ zuEZKu4s5K*x0n|GV{Rna7|j0wN(WF)l?Y6Yj54^}oal*MTwK8ShwL}de#~s0ont_B z;(`XAF>uAe-Nn+~U3IrCw04rr&KBVy=Gwptw zG78*gKrKJu`aA+tnug~NMxOZwL+K3irFEceR8~~T3)0hu_+^=I1>A>eqQ)Rp(eU;B z7#L^=z8Xx`K&)_6Qt~d>E2<)cNW|2x?e~D}C(B1G%2{KzEd_E_bnRoq!z!@Qz<1Lu z#1jIT;~xNWNomD*DGy5Ma}t-LN#r){2EO~Y`8Jf{IS1P+31X>E^gC@%X zY8iBzM0sWc$^fiLUS8ghH8lYLQ?SuTcja<_ggOmqHV`|&7*Zl&x8lVxuGJsm5l41oL4D827;t*qHy?vt8-@X2a4b zTqU@5I50qT0&yF#75*6&!WATuO5o)N9a4g$Q8}u?YlQ*EzmVC1fCP38HT|!Gy+w8WL+?s&!OKP=}^ayV~qKGpfdrHfuhrEn>1~1 zJcJ650tF_J;0HzevgNe$2FyJ?s0Kxp-IM8(mShOdQ84NciZ^c8qe-+vzgr6dWmJKY zh4Rn)09k=_b*PMTOn9OcEA?rqJ_7bA&kEj3iVmZEtF~N^1nl@PkO?3VU^pkG;cAu7&@w<-KD{xTHK!%4PgA`~}&l{Kq)gZf#whk?Dt_@4e zbf%j(w^~_b#TEx~>Z+gzrVnS!EoymoK13d`IIYwtG`F`WMzy5LO$-lr&(7wqJ*WW& z4mYjRK+?9ruZMCJ8Aq%TbfezVH7q4v+$pa2$fOx%46zOoW-GbyuIN&@xT1 zt+3J{h^1712Jp#0YtFQcjH{HyKPD&pMn>od>}fRycmL+aOSn%ztx&>Cu0%ySEosO* z&3%0@13lrhJJ=w?3xkLN0y0cD?P8I24@Hd?BE=0FK>% zL?D110T%y>LPZofzy`+?_yCy&H>xjo1sB)6%pd#-RMvs_B}n|mHw~tr@L*f)J0f+1 zg<92cdZ7k|niImcMI0nIx`HuSM@Tf!J6P3l!_Fg;t3Wdyu!o9SS?*MS@O)+o>c+i6 zksJsAn`EQT$=ci;>YC{V6MDi6p^Q^nrly*$3v-*_UuN;H< z4q`%LqEJZUck`)RT3XY_xSFsT@`fsMGL)0BZ#X9>AH9$$=?K#~+2xUk_qtQ~UYH5qkjCm7WC>Z-jp^ z8Stz`etAsT3XXMjJWDkv=-?~GG;*Ub${YR;L8b)R3nkFALVL}%s+`C%W4HpTuZj;| z9m)zQ#UPF^{7Ks1whrm(&1mp%mR8n4Aau?TI$dO>h(IluCt>!Z(zC8JQ?>{4dzN{_wm=f4+t|iJw4Nq^5F)%+gl?@0Oo6f)m z;t?cBS7LJycfW> z7^47g#Qpav*QN{)IuFjh*q*#q$8)dl!JB~HdZvKQ|2_(Y5Zm!6|9oSgZR}Ja&IFJ# zGsbd*@A1Ul%rWIqLN^+|12C z3CLjPYJH~v9BEx){z!CGk$x-C)F`yy$;n6GcjAU|oq^yrhFB=1K-maKXEOLC5c>4` zCyUhZ$zCIEU{HgvRvU~pMb1lkF3~1(0OhGrGt&}!02p5xF{JLT%~(9B!JTrbF)^b*xS_!PH4PF+KKk^Qu3DOUIg}y2Nb{Bj?7Qpeh+p0bgfQ(@mla zMw!av$h1#K&;Iw-__&bj-LFz-Esr+1cOzA>NGan#-1j2_U@F(stP@H z2+9d=Ct;^2rzQ;()YyCw>=NKCC$=(JH!LbV5)dFt4oaSx52}TM0sdJe zwxXCyoAz08)KdNXL?sbQ`Bxkw23#MCzm!GKkOi<@G2b-V8J3a1(D8hs-G8*@a^QlOB;z<1W0tG$ix}1Ih`CAiHhd*J!O13;=bka|{N7j` zmC%e}H<#4>-fqB5Sssoo?DUYv4sUB_ZF9oZVzP_TvFp%k%kfvdw1o65lvW4?aOj>l z=W;{HgYOR}EGz$JwNw!7XqA^yExqevk`bkrO}2~~qm%#QN1zbC!N>&Mv}8}ZI!UPT z&%&*``Y-;EA2-VgcedRMjB2=>b_Q?h`}kH{N5gMUfIJEp7WP(05P*UNO9aP_I;MHs z+Fdp=wP-NCZFH2pw^qrVk-azt-0cv&AYeu?O`Sn45hh&oZ^mjhigdYPR8@)?cz6|y zK;T)^5c!vupiyD$VWW<%`dF`IWJGF$3b{lxNt9m8hXk5CWNPq*=14)ZsZqnj!qEeo zD}LId3s5dQj~I0%l(qkBAoJ%Wpo}i9r2P13j)JLjLM+Ww6IXnCpPq#&nQrX**9~1d8Pcg+k^Q|^RX@bd<)wQSOx9#yLs(dwPLge zDk|)5zas4!`geAyABx^BXs4Nw0mwY{lDU}?1KKlz^Rj!9;rT>A%E8J}tSxsc5x;pu zpUezqZZN)ydcU#krl_cfP!c@N&k6>PL=wiRw3`$IS z6t^DFmqfz)_vv?yc*0i3yXV_Ne1Z5VqH$Y?7RDZ6^yfL0OG!=yj7*q_>Qs`l(vnie z_k`HSIdjuGZrrCm9rBcz;ZP!M&edI9IKltc zRi*aqQF(x{R_l1g$U(yQDT#y8n1l8G;x9&{wR?o2`f&nlf#b}H921#8U=U4Tpaz8Z z>GmVw&x>eul3xav7C4wCIReT0Od^!{c8?xK#l?B$%IMt97lI-Pm>bSLfJxk$FYv3+ z^lkGjz^2XL&Y_H(6BHCA80YiJ;URY_dydt#{{xuL2{0w#VmcZbLLIG2j1y6 z<646S|KeSvO!>nXzY~=@1uLAd80?Ux@9#dDDGO`mP2qRs1}6D*-xqC1ma@4K#^z>X!EjrphXE|mLqoif7chB|Sq z|J?9vDK}qCtgFcs&q#~Gk>9IUw?F@O(iC8fDhPxz!X_`Tu`D|GJy3lrzYA)__#Ius zJSBU7BVh-)?iC-xFZkl>`0egsG{{M=P!A(1RO)e;Nuq`0^PL^NP zf@uH=%0rOcsu!n}%n4uy^a%EioK}^o?&6JQ=q(H{DTD|xsTIpO3s3^gfnIWe|9_nsC<*+(NRB>u>i2+lL_2t@l=={42 zy@YHXxCb|6Xy#Qq#A@dj4Y+zWQ!%1ToJEV_673PMP{s=yn`)k4|7SU(dAQVJRGafB=C*ASfa7v<$8n=zZ(R zGcn4)v3L6M<|eRRqf`_c%RF|LeZ5x3)LKlzsR@{9?o27Gs~;+;@y7GeSV7qXq!zpk z0KgG|C7?tBY30nNWDd30W+^rX<21-BXl{ToLlyD6k-xDF97mz)hoI_==AtFNDJwhq zo&O$JSCDLZ{ffGoW7_XVkzRwU+<*-OKV%T9+|8tQpuQ;STSxPjJmH#{EJxOmW%b;_ z+5f6C$pLqh4IZH|3A`}9X@2fyzdDt-F8&a5z1#TpKcd- zGIJp{EigALw#Jo`MXGc4~hZYi@7}@;Sf~;P83BsG`39%Wq`LXv5=c z;AI7E_CvUaDCTGjyN`KG}wGu(>qS_o`3z zpkw~G1d~wdNAr{}S34Oef|JaQ85j0{Be!g-OaAN>n2lHQ!F(Ki6)b+8x2yz#7X@O- z2RpB3ZNQeXGVf$|Br3Z*O@6*4#3B`di84@Jewn%5E6FiOW8C1E1oXDoO)c#goZ68i+F}wn(-{$e5LQY|)E~3jApfe8 zP^z&}VteM?FU?E~ON(dP`t4uo2;izI1mdPf^(a%h>B4AXHajhaPMIzs8g^MhQ6PL( z7GYpZx(kZ=7sbW;$z5(I!)|Ns=UA%z`^yT#6q0?nK3{F#Uy7&CP6cz6eXg*8kh?W^ zIT*R<$?tPt0(li(Q7s3smaBmHc-$p-QV0WuODV~qB?fA-Ut<$U7;c~23H{YfG2l*U zfS_ljmq#E93hYK|9zTbxhb9DpoBXz|E&GX4RHMbCztg7*3hlmF|Ag&Z3>gflLozQIlPyLes=^h}xE zaf2VomkeZDe&gxFVet*yO_zxY(}e@oI%=890&(%GMwjTyOmA10G|Pzf=pFY2cTI+# zdul6a(lSZD^q4UM0JqbF(qYrK*B?a~)~HDQRCjR|VE}jl)97>>1%nonu+HbV#jA;= z7rLMk?y`)WnPGyxpo?ImB#ttdx=`zK>SsDVKf2bPi4HON`Pdn$XwV|YoQ(!8G;Agb zajjN~xKJAdMG1eI`ygd?_+`2yZzURc59LYTNsxYa7`jYApmU>&7cQ-3k>Gt`V&z%k zVxvo>oeKtQ_kPN#j-r=hj)HpESh!3x09XA7AXGrX@MmsXlA@UXmFzw(Qtp_dCLpZx zYy61E;dQpP_5#twPZ=v`oDO?UKTC-u#Q~p@Ft4uV9A0x|1+c)iyH4Rx|1mHI{NG)t z)c9UHykO@2y{1IW^qhEkHgtj7#)%y2gM_YLca9`Js!YQ5O$1|OW6v!I{NkZuz(X|r z1rAZOF4=Y=*qOK?&ecE4NAC{m>E9N*v;71yUPR&kXiIa7W)vWp|YK$=?_|8H#o(TMiomDM!&De9|T zdBGDXM9Jd&MqDeiBdTumuimk6*;H!YO97n{hLS&+jM6r(Ea|U6E{0JL*esV}AU1XP z^ni;a<*8xt+rkWCtY)7#V=t{==UMJTrD?!h7nXabQ5J$DPe%FZ;m@ zo$6En!-r~xT)S*-yJN#i?CDj`^dw#OnPwnvNaIBpt?##eYL{*MzW0u%Oh2ah}#sm{M`m8PCDP;y&d&YEowgI z%fcCASFwnRY*(5O#f4dZjwkzeC{N|vEv>B-Ca4)-=HcOC?b3%jB|`v4`2CR&`EU5tA0Z$q{16oX692iRwah=hZBA*eihh7crmqLFSH)VcB=bJ$1%j7lQ zYq5a4szz;nR*;-Y?@q)`S<#R*yMDn`HAS7kwQ2Jc^$>@2xb*0h6**XQ0;q#m;D9;} z?f}bhYb8N0eduk2DFu8akcPqZnN^J%Ca%)`w*~1xC-G)ZXiR}qB>|MGT`*OF&zQ^o zYmG*(**cz;c($rC_$%|mTTXw0b^@QwzI)dkDEjZUF~7M2y@ZGc=8)S;8-e&Rvv6-8 zUaw?mLyGSi0Rc0env#O9A}ghj>D1)K%h+bumRxT8Iy6A8-zTKv&a_QkKzR<>i^To% zVB_bH$!t$-nR_(OERKKN1idyNrZ1ab6Pw1|QP@3AuLyzoIjIScUBk^#S-bQ<_of!^ zO;Z?&^^{L|Vr>)6ldnZ#{w|28dS{QoQo zHr{zWvS$i6{%IFJuf&!M_v;t_=-2tPcVWqx>B-URDIfmAQQ^Yi7q2;s$qmZmc~{aH0?jBm81O$+J8U;;WjWnB-~6UNPY%(ky+sO z#ls*uJ|uO`0`hlNefO(tT5uf-6jLvNlSChWs>h?ErUpOz21w7V!N|~15o!ijO8FYY zum#^;u(br23aUsYc-7k4+O@642LtZ?eSNvc{CTG3a3$YIw2O4$J}m^2cE}M=H6144 zR04nYZvN>`!OtpEjb)t+n_vJ2GGX{pIIKK^TT$9R|Nnwlt0dd(9t9n4mj|CEI9$?e zqfR@DXO;owN>2|a{?&r@$``6;);yOG@+C{o>nE=ek z-xH45ivV*4UVhSN?mW=fE&ctk1huh1Fj0cVC~yFW6~H>5ThN=wM1GNn^#%bq93Zd* zxC-2Pyx%8;uS*$gls$zDCIxdl_5EZUlFaj-(qAw>TO}xiPW@}-+5HQZRaNZYc1~x$ z^kb#`w6w%Cj6&(~T>?akDpmzPkf4aj^OY4Bm<+$wy`7hwC$jSB%H`>9K#ks4p1Rievo1cj!uUxrec`QRuu82Uu*9@ry z_%0*00Ct0X7e;X)x`YxaM_ayRNRd53zG*_G#WcbN4HZ!C%Xk+wupOXk|1>tn0G$?i zvUAf?9}of&f{F=LXgS)%z)$)8=rYVH8}@yq<3Pp+j3fNe+we?NQxpC=w}tFritPRf z99%mSb92DsdeU9^MF7H#jEqv>ym>x_1?7g_@v#6y%uSxY+mwWr*?K&EkH0-R(WvFS zH=YvT(Im6m_o%qIV^dlp?o4+oed*>iS)kcHm!6CyFs0pJoNVY5B=-;iZjum_mHLw( zyZm0w$pkFx$SS_Ei^wXaMRVp3oV3n<_>KL}fG!%gs&fdQ?!e z!XJZ8_$l8fFu67`8x-l1*}aKd=9l<9v$@m!mXaVL&mH{3VBf3cRbDeFKWNzPH`zK_ zu=|>ioSecc{ueKPc!)=2$~o;yjb^t9AcbIrum-zz7Rqz5>!6MmvdUyQj>?sXp!)+| z8>`CkY04Xf}y!gmZ< ze0`%q__Iv$pjV**cKdfZCxEiJop2Ll8Pb%G?Ag&+=Ni5O}C!` zwS9o>1+_#7K0jRRW;XZ0`UJ*`fcwx}rDkRV_w5tp$FUVLpzq+LDlqtg`2`ONp${+} zI1^MCSxqnSPYgF6{4X0(NUr{=*tCnClK`ib-3e^aIRc0?>^E1zT@!yMN`^XMB&AP5 zM-efDJAgn82^tV-I3(5vDjd}9zyJWU0611cH}O)PE8(m4XQ768*?nRsCnuPq1BDu& zwSh1Rj`|=31jc#A(ceoXWK4#LszA&qi| zfVYG0ARQurBzLjP56%LxGz29jqjGZCDz{?kT1bdOJ0`05U_(Lw0!sL|KxARSCEKuF ziiV4&-cGn$1qCsKc{FSp2${(??U39IZ@@KY9SfV0+9z{8@qlNCsdBL2#qmTvtEmx% zIUV3gXNz=6gWAm0d7(wr@r-wC6_%5W2PIqGRL~XR8$nCKa156$LI?(lGZC|lAECw| zY%iEQzV5_YUn42 z0apsP9;gpg@<3v&gpdUqaeNQ#+KP`K@!3?D=PVqY!H>ZqekpnCP7-VoE z$GgSnH111o-#0bE`Gz~Yjo9=~gBZT(+aAVL^uM_#eRvGB#@RtFxiIjJjG1(-0hs(L zP+~0X?9M@H3xDvW`&Zd+>B15cc;2Nd$7uutpIS?X-WKyfBD$NOs9(ZD5i#IeSJ~J| z3iskd?0~*ICN|arLLHo&Tp=^aP;gq{@BxWU$zC?2R}rqc^xGoFU@(@q0UwKi6ZH1g z>-yT-a9CY%hA7fq15alwXJ>NQ5n#UN?M;U&H#&0SF=bT6Rd&vSi9#$O3>0oCRZ_bFF+O?-)LaF|EgWccRUk902V1g?TG;|aog(1kdRY1r~0HWKqCh% z&s;Vp1_lr_K#g7RiAna+0$c~MXs}pDUYNqDZ1XVyxK%a`|1t;4?9(2#GXL)`x zy+fZjb^sQd0s~qQS7@%h6_)(NM3vRjPfzpJHN618+%Y#H0o?3yHMpdU07ifAQ%zRN zkxlZ$G+&c|tS1Y&17%;+Ah%9IB_URUIi%C?O_j7d&WRFIpTe+m`j;GP&30MZBaSr? zkv~iQIUsbu^S1i6PO=RT(Gzw0yyc@TA0}^(ep5cAcO`46FTN=d2qMr+ef8h= zGl?0-Y&Y7A$dtc2THV~EjP8QS4t^i>7;E&o)29*?`cuws5P1dXGP*VIRmla##n=1Z z?$fs;wLHP?-NZ_t%G^H~uRid-b?%n->8Y<0+hYfJNYI`q<2uL0Z+_*QwZvWd!vXoe zd=s}+pm$!Dpt97NgPQ7mB_9&FYCc1U7IR({iR4%?)1Q%t?ApA9~ z)RE?1UYEixreWR=uJr^25V*EOxQ+0U! zPr1E$nt+Y3*I?#K+us1qB!M|p2SDT*86Cy91|S^?ZpY%lw{sTgw(O)pmxKWse!L%~ zSB_G?H34R01fSps$vZ!Ru==;RiM(c@=1IK_W^*YLXmuqqO8AI}323G``0m>Q#``O7 z!DIxMCC00?@jbAL)3^iZ1rjt1Jut&JoLybPxverQ%Wf^A(*klYY)*I-NZ?BBr$E*Q z#2^f+d=4T}7wB}rx9EM=@!9A9F9|~7NB}T|a*=UAhNBZG)cVPsOVcIAQQ)!##T=Zw z$Nic<&FL)tAF)9oRl@i1YShqo<#PKw9?_J`#Z*p4;QW2n|5aZ@{WF1YX=dHTGW?S< z>zq#OJ^}rIr#u)TY2dG$gadgA?g!6$E_-}=GSw-aOOgyCI@F%n_4B?y3fU}v=RhN^ zL%ZIO#j&Ikt8OJf(3+R}TdzkViSGrKO|QwU7Nq1&ggRo8RWabo4+In_kE3G(TeAWo z5TzHe;dSf$@tSMF5Snak@QDa|3dlvebJZ?az+p$58<^B^i|=V*S?4|;U~+&SCvy*g z2V8Rrd@+9;DU@|2^S&>L;`S0{XAgag25?z))&zbB(VCQm17y z?0zs=!S^JFrSR2qM?G__Aw_xKl$It})_K{#;IYuh zw4MemXfUBu*VKglN{L3Jp_YO+0Nxp%>$Sj-t2-k5F*m1uFT4cL z0b~s5eO3U1!{E7!Zw_`h6kc$>DI`!RzW`~3|I{}%RpCjZ{_Vx7cG`*@G6T5r5y_q$ zK88Ls>iSb?Y2dNJxo7T-%|YIX{euUBVeWWeiyT?P#O@W%*BH`VP8|1bAqTXVSu zdP_X}Ydo{B5kK=W8D$xs67Kz^U)UaeTvw*2qm|Wik}-8i6mwIjsLYuEDkH@lD6!dj z+u-kTn1QPUJN(R;YD2 zJ0J%zqlksOBD8+>0P_u6H&|P^nh!2Mha8Y}*TTvQ4>wqaEP+GyA4TkYW z>82M(U$q3&|N25h=rM5XZs%ml^3l=b{kS=}AsSF;*taTqY*o9Lt-!IwzQ%BO11aDE z3PiAVgn>8AIUQ?w#Ol|jFGS^nixVsq9Lci&>Z6R_f)})qzhR1cGot_hv~}(AQ09Ah zy1A6rR#T>klA8D^spOJ|66z2UUCuU>rJ8CqDUzTfZjJl~sSm|Yf`RWuVI4bBX}&Dbyz zgRWlLqO-dBn&H#N^5(I&in_7rJGvV^zn?x}SEWu_JRFJ*i-zbTyi9Y@usamSew`_j z%+voDh{=W=+>z*f0o*QXBI`HfHb>GmYSUIe{D~4*SKgs!gcU@U+g}N;sh;;0ryU66 zXS&|=STM=;J5b_SxQIj59-MtTOnBz%XhUkMM41@kjvJSpeC<0;a!Fc(V?yHmyO@8jjgS*N7L&L7e%a91hp(s zpnGS_n?F(oa6b|@5=elx1mgEEOsmuGUKTy^(7fI+A=}3cqYNl2;%wRK@3Vagdqe<1gQ2e zywFk{TrD5z#$u6Q7%{usKjoB7qKEG~S#kA*_NZZWl`t@OS#o zPhjOPz&E>l&mQyyaN64FI3zgTJv~Rw`cAnByu;MDx(XF`vLV%Tu3&ZP9=<$!0YefG zL2t)Aq5%`qu}(Fo+Xl9WZ_zrqJr-V+PIrc8hib$6&pEGF0%@Wb6GF>|-_Qb!|3^qi zt*zt?f{P;4M{p4UZz#+d&yC+9alIF7ilD3~xvy}(1BRWQZ-%YR)*YwMVlt>(U#HM| ze8Aq*XZ+3j9qzjT{V^Q4DsD1EXpyB&coPYIur?mARrfo)4Flri0D9g*& z6y%w9vJG)LxI9V#YKC2*{Nx@5xiDUu$ov5PaN0CnUbzDwajGdv)uqN)z`5Qrr$*EClX!))PyQ3}2yrDuTouC#YR<7&0|b42EO zR2D$;K__QYt1vp{eE%jwT;F0c7em^siX!HdblVgj#5v+IyK`eEgs zv3+03jy47^IPef(uyg@=3j}UNh1rnBp0q;xh0*(%UEfqcghdaSX9%eRiNt4lo94z} z5WPKSg~Nx)eYnUpDuqwMu|tN#|K$XP)Qit@wynpU6IeY4>EmF-D**Zesyg0Ynkei> zl)_JNV)5|C+4sHgz>%z}z4G!fcwLQ>J1L(K>5@=+W*+@SQ+7WMg2N1DFEuqP(v>=7 z=L!FfWCljMMvy9vCi*oCpUEmKITRM0ZfkiYDw(?wxcw5kfL+LLp~0T)cIfA%j5$$u87?*kwHOVEjYV+< z1*_mN(9s3`8h=Oe7Xk<7Y-3^$dZE3|O>t^ro8XI}LO|WXL!Qsk0t0GnYr`7r`Sm*5Gv!1+NW476b`@Vy!=mHv zm@lgb^|A)L=)^2KRw^OZrsWdc(CYwCeorq^2&DpxGoh%VQ6x_+y4wx*k>qSN^Bjb5 zG%`Tle)kAl&y=lcgXI@b8!-i2ka44$d4= z%0&amz$9_UqNW;*MH+xx+-tthVBta{8KtjZ4ZjYrfTlK%N6-{ni6lvr(SKfVYOk!1 zet2aB54oiaN~d8OuUK}xk`mz0U_e=xvb>%oGT$p|HezSj@nSqT`~~nM;s#1HaUM_8 zczLfxZoPvM&j~EN`~}G&te*rY=f7U0e+2mg2>$u`k1Hf>_+m`a__IqPDKd96F&sTj z0{}yS#G04D;NY{Caqw!WHkwJALUa{qpGnV+zdqfb&k;!#l9Iklw1B|io1RPU!R@$R zZ#`G5MlXB3d&_lEuc5RQuk+J;V9p&HujIN%Ag{CGMUO{#cSrZlLn6gi2`4%;dN(Fq zXw{|42Bd?v8EX6BE2WD|lhwn#P`VzZ{``EvOE>_#xj;u=KTt>Zmi;_cWiTzGX$f)OVV&auD(hD<~c#;x_5WQvS2uIxZow?U|SFhjYB83Mw>+E+IE{6h|#Zo(LPQjYAF zlKRJFrjKGkUbW;l(P=R93C~9Pc7ZaPxiY_dD}!IGcW#7;&RO`B`+fK-v5lNL zQ9cHhbMY_kU6BM2FCnik8?-a!pESY&sm|(0NUYKR#A0UeSxg-aFDskOS!HT24OMUtdAk`S>rdpy;IuUA;O&Te-|77K3(uX;otcv1FO<_h^wC+fQQ;O?(l^ zw|5a-Dd5EGx=Qwz$*1_*{P~fVo`Kbk`|7k=f)~JGUs!`D>AEzY&c2kf7k3YI=&>U! zzMX`4J@ts4gOi0Iuq7^_rSryH)$;9j%1D8Sd=|-MdyJ>Arz%rz)O7!tS%G)YauWV* NclOwNZ;M~de*mR9_|E_U literal 0 HcmV?d00001 diff --git a/addons/mail_bot/static/img/odoobot_discuss.png b/addons/mail_bot/static/img/odoobot_discuss.png new file mode 100644 index 0000000000000000000000000000000000000000..18949ce9f9e0c658149c01ae3a7de2c22daf7169 GIT binary patch literal 30404 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 literal 0 HcmV?d00001 diff --git a/addons/mail_bot/static/src/js/mailbot_service.js b/addons/mail_bot/static/src/js/mailbot_service.js new file mode 100644 index 00000000000..300e95d06db --- /dev/null +++ b/addons/mail_bot/static/src/js/mailbot_service.js @@ -0,0 +1,28 @@ +odoo.define('mail_bot.MailBotService', function (require) { +"use strict"; + +var AbstractService = require('web.AbstractService'); +var core = require('web.core'); +var session = require('web.session'); + +var MailBotService = AbstractService.extend({ + name: 'mailbot_service', + /** + * @override + */ + start: function () { + var self = this; + if ('odoobot_initialized' in session && ! session.odoobot_initialized) { + setTimeout(function () { + session.odoobot_initialized = true; + self._rpc({ + model: 'mail.channel', + method: 'init_odoobot', + }); + }, 2*60*1000); + } + }, +}); +core.serviceRegistry.add('mailbot_service', MailBotService); +return MailBotService; +}); diff --git a/addons/mail_bot/views/assets.xml b/addons/mail_bot/views/assets.xml new file mode 100644 index 00000000000..134b54e931e --- /dev/null +++ b/addons/mail_bot/views/assets.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/addons/mail_bot/views/discuss.xml b/addons/mail_bot/views/discuss.xml new file mode 100644 index 00000000000..e7f0650c867 --- /dev/null +++ b/addons/mail_bot/views/discuss.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/addons/test_mail/__manifest__.py b/addons/test_mail/__manifest__.py index 40d43883d4b..4f76d51cac9 100644 --- a/addons/test_mail/__manifest__.py +++ b/addons/test_mail/__manifest__.py @@ -9,7 +9,7 @@ 'description': """This module contains tests related to mail. Those are contained in a separate module as it contains models used only to perform tests independently to functional aspects of other models. """, - 'depends': ['test_performance', 'mail'], + 'depends': ['test_performance', 'mail', 'mail_bot'], 'data': [ 'security/ir.model.access.csv', 'data/data.xml', diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index 2d63b95f205..017a983b515 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -15,4 +15,5 @@ from . import test_ir_actions from . import test_update_notification from . import test_discuss from . import test_performance -from . import test_res_users \ No newline at end of file +from . import test_res_users +from . import test_odoobot diff --git a/addons/test_mail/tests/test_odoobot.py b/addons/test_mail/tests/test_odoobot.py new file mode 100644 index 00000000000..b171020244a --- /dev/null +++ b/addons/test_mail/tests/test_odoobot.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from odoo.addons.test_mail.tests.common import BaseFunctionalTest, MockEmails, TestRecipients +from odoo.tools import mute_logger +from odoo.tests import tagged + + +@tagged("odoobot") +class TestOdoobot(BaseFunctionalTest, MockEmails, TestRecipients): + + def setUp(self): + super(TestOdoobot, self).setUp() + self.odoobot = self.env.ref("mail_bot.partner_odoobot") + self.message_post_default_kwargs = { + 'body': '', + 'attachment_ids': [], + 'message_type': 'comment', + 'partner_ids': [], + 'subtype': 'mail.mt_comment' + } + self.odoobot_ping_body = '@OdooBot' % (self.odoobot.id, self.odoobot.id) + self.test_record_employe = self.test_record.sudo(self.user_employee) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_odoobot_ping(self): + kwargs = self.message_post_default_kwargs.copy() + kwargs.update({'body': self.odoobot_ping_body, 'partner_ids': [self.odoobot.id, self.user_admin.partner_id.id]}) + + with patch('random.choice', lambda x: x[0]): + self.assertNextMessage( + self.test_record_employe.with_context({'mail_post_autofollow': True}).message_post(**kwargs), + sender=self.odoobot, + answer=["Yaaaay that's me!"] # no the perfect idea to base a test on a random but it should work for test phase, better mock random choice? + ) + # Odoobot should not be a follower but user_employee and user_admin should + follower = self.test_record.message_follower_ids.mapped('partner_id') + self.assertNotIn(self.odoobot, follower) + self.assertIn(self.user_employee.partner_id, follower) + self.assertIn(self.user_admin.partner_id, follower) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_onboarding_flow(self): + kwargs = self.message_post_default_kwargs.copy() + channel = self.env['mail.channel'].sudo(self.user_employee).init_odoobot() + + kwargs['body'] = 'tagada 😊' + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Great! :) Did you notice that you can also send attachments, like a picture of your cute dog? Try it!" + ) + kwargs['body'] = '' + kwargs['attachment_ids'] = [1] + last_message = self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Not a cute dog, but you get it :) To access special features, start your sentence with '/' (e.g. /help)." + ) + kwargs['attachment_ids'] = [] + + channel.execute_command(command="help") + self.assertNextMessage( + last_message, # no message will be post with command help, use last odoobot message instead + sender=self.odoobot, + answer="Wow you are a natural! Ping someone to grab its attention with @nameoftheuser. Try to ping me with @OdooBot." + ) + # we dont test the end of the flow since it will depends of the installed apps (livechat) + + kwargs['partner_ids'] = [] + kwargs['body'] = "I love you" + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Aaaaaw that's really cute but, you know, bots don't work that way. You're too human for me! Let's keep it professional ❀️" + ) + kwargs['body'] = "Go fuck yourself" + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="That's not a really nice thing to say, you know? I'm a bot but I have feelings, ok?! πŸ’”" + ) + # we should have a default answer + with patch('random.choice', lambda x: x[0]): + kwargs['body'] = "I'm batman" + self.assertNextMessage( + channel.message_post(**kwargs), + sender=self.odoobot, + answer="Mmmmh I'm not sure what you mean.. Can you try again?" + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_odoobot_no_default_answer(self): + kwargs = self.message_post_default_kwargs.copy() + kwargs.update({'body': "I'm not talking to @odoobot right now", 'partner_ids': []}) + self.assertNextMessage( + self.test_record_employe.message_post(**kwargs), + answer=False + ) + + def assertNextMessage(self, message, answer=None, sender=None): + last_message = self.env['mail.message'].search([('id', '=', message.id + 1)]) + if last_message: + body = last_message.body.replace('

    ', '').replace('

    ', '') + else: + self.assertFalse(answer, "No last message found when an answer was expect") + if answer is not None: + if answer and not last_message: + self.assertTrue(False, "No last message found") + if isinstance(answer, list): + self.assertIn(body, answer) + elif isinstance(answer, tuple): + for elem in answer: + self.assertIn(elem, body) + elif not answer: + self.assertFalse(last_message, "No answer should have been post") + return + else: + self.assertEqual(body, answer) + if sender: + self.assertEqual(sender, last_message.author_id) + return last_message diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index f79c235e377..a3816c1de29 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -166,7 +166,7 @@ class TestAdvMailPerformance(TransactionCase): 'default_res_model': 'mail.test.activity', }) - with self.assertQueryCount(margin=1, admin=9, emp=13): # test_mail only: 9 - 13 + with self.assertQueryCount(margin=1, admin=10, emp=15): # test_mail only: 9 - 13 activity = MailActivity.create({ 'summary': 'Test Activity', 'res_id': record.id, @@ -182,7 +182,7 @@ class TestAdvMailPerformance(TransactionCase): def test_adv_activity_mixin(self): record = self.env['mail.test.activity'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=9, emp=13): # test_mail only: 9 - 13 + with self.assertQueryCount(margin=1, admin=10, emp=15): # test_mail only: 9 - 13 record.action_start('Test Start') record.write({'name': 'Dupe write'}) @@ -199,7 +199,7 @@ class TestAdvMailPerformance(TransactionCase): self.user_test.write({'notification_type': 'email'}) record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=56, emp=71): # com runbot: 56 - 71 // test_mail only: 56 - 71 + with self.assertQueryCount(margin=1, admin=56, emp=75): # com runbot: 56 - 75 // test_mail only: 56 - 75 record.write({ 'user_id': self.user_test.id, }) @@ -209,7 +209,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_assignation_inbox(self): record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=36, emp=46): # test_mail only: 36 - 46 + with self.assertQueryCount(margin=1, admin=44, emp=55): # test_mail only: 43 - 54 record.write({ 'user_id': self.user_test.id, }) @@ -253,7 +253,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(margin=1, admin=47, emp=64): # com runbot: 45 - 62 // test_mail only: 47 - 64 + with self.assertQueryCount(margin=1, admin=47, emp=67): # com runbot: 45 - 65 // test_mail only: 47 - 67 record.message_post( body='

    Test Post Performances with an email ping

    ', partner_ids=self.customer.ids, @@ -389,7 +389,7 @@ class TestHeavyMailPerformance(TransactionCase): self.umbrella.message_subscribe(self.user_portal.partner_id.ids) record = self.umbrella.sudo(self.env.user) - with self.assertQueryCount(admin=79, emp=99): # com runbot 77 - 97 // test_mail only: 79 - 99 + with self.assertQueryCount(admin=97, emp=124): # com runbot 90 - 113 // test_mail only: 97 - 120 record.message_post( body='

    Test Post Performances

    ', message_type='comment', @@ -406,7 +406,7 @@ class TestHeavyMailPerformance(TransactionCase): record = self.umbrella.sudo(self.env.user) template_id = self.env.ref('test_mail.mail_test_tpl').id - with self.assertQueryCount(admin=98, emp=130): # com runbot 96 - 128 // test_mail only: 98 - 130 + with self.assertQueryCount(admin=116, emp=155): # com runbot 109 - 144 // test_mail only: 116 - 151 record.message_post_with_template(template_id, message_type='comment', composition_mode='comment') self.assertEqual(record.message_ids[0].body, '

    Adding stuff on %s

    ' % record.name) @@ -476,7 +476,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id) - with self.assertQueryCount(admin=58, emp=73): # com runbot: 58 - 73 // test_mail only: 58 - 73 + with self.assertQueryCount(admin=66, emp=88): # com runbot: 65 - 83 // test_mail only: 66 - 84 rec.write({'user_id': self.user_portal.id}) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) @@ -499,7 +499,7 @@ class TestHeavyMailPerformance(TransactionCase): customer_id = self.customer.id user_id = self.user_portal.id - with self.assertQueryCount(margin=1, admin=155, emp=184): # com runbot: 155 - 184 // test_mail only: 155 - 184 + with self.assertQueryCount(margin=1, admin=155, emp=190): # com runbot: 155 - 190 // test_mail only: 155 - 190 rec = self.env['mail.test.full'].create({ 'name': 'Test', 'umbrella_id': umbrella_id, @@ -528,7 +528,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(margin=1, admin=94, emp=109): # com runbot: 94 - 109 // test_mail only: 94 - 109 + with self.assertQueryCount(margin=1, admin=105, emp=126): # com runbot: 104 - 123 // test_mail only: 105 - 124 rec.write({ 'name': 'Test2', 'umbrella_id': self.umbrella.id, @@ -566,7 +566,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(margin=1, admin=100, emp=119): # test_mail only: 100 - 119 + with self.assertQueryCount(margin=1, admin=111, emp=137): # test_mail only: 111 - 134 rec.write({ 'name': 'Test2', 'umbrella_id': umbrella_id, @@ -600,7 +600,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) - with self.assertQueryCount(admin=54, emp=75): # test_mail only: 54 - 75 + with self.assertQueryCount(admin=54, emp=76): # test_mail only: 54 - 75 rec.write({ 'name': 'Test2', 'customer_id': customer_id,