From 08bfe9d8c710ea9a2e3e76a2c3aaa729c09fb3d8 Mon Sep 17 00:00:00 2001 From: fredfishgames Date: Sun, 31 Dec 2017 17:26:13 +0000 Subject: [PATCH] interactive bots: Create Salesforce bot. --- tools/run-mypy | 4 +- zulip_bots/setup.py | 3 +- .../zulip_bots/bots/salesforce/__init__.py | 0 .../assets/link_details_example.png | Bin 0 -> 16364 bytes .../bots/salesforce/assets/query_example.png | Bin 0 -> 32311 bytes .../assets/top_opportunities_example.png | Bin 0 -> 15520 bytes zulip_bots/zulip_bots/bots/salesforce/doc.md | 80 +++++++ .../fixtures/test_multiple_results.json | 26 +++ .../salesforce/fixtures/test_no_results.json | 8 + .../salesforce/fixtures/test_one_result.json | 17 ++ .../fixtures/test_top_opportunities.json | 26 +++ .../bots/salesforce/requirements.txt | 1 + .../bots/salesforce/salesforce.conf | 4 + .../zulip_bots/bots/salesforce/salesforce.py | 178 ++++++++++++++++ .../bots/salesforce/test_salesforce.py | 201 ++++++++++++++++++ .../zulip_bots/bots/salesforce/utils.py | 47 ++++ 16 files changed, 593 insertions(+), 2 deletions(-) create mode 100644 zulip_bots/zulip_bots/bots/salesforce/__init__.py create mode 100644 zulip_bots/zulip_bots/bots/salesforce/assets/link_details_example.png create mode 100644 zulip_bots/zulip_bots/bots/salesforce/assets/query_example.png create mode 100644 zulip_bots/zulip_bots/bots/salesforce/assets/top_opportunities_example.png create mode 100644 zulip_bots/zulip_bots/bots/salesforce/doc.md create mode 100644 zulip_bots/zulip_bots/bots/salesforce/fixtures/test_multiple_results.json create mode 100644 zulip_bots/zulip_bots/bots/salesforce/fixtures/test_no_results.json create mode 100644 zulip_bots/zulip_bots/bots/salesforce/fixtures/test_one_result.json create mode 100644 zulip_bots/zulip_bots/bots/salesforce/fixtures/test_top_opportunities.json create mode 100644 zulip_bots/zulip_bots/bots/salesforce/requirements.txt create mode 100644 zulip_bots/zulip_bots/bots/salesforce/salesforce.conf create mode 100644 zulip_bots/zulip_bots/bots/salesforce/salesforce.py create mode 100644 zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py create mode 100644 zulip_bots/zulip_bots/bots/salesforce/utils.py diff --git a/tools/run-mypy b/tools/run-mypy index 958354c..ebe9003 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -78,7 +78,9 @@ force_include = [ "zulip_bots/zulip_bots/bots/mention/mention.py", "zulip_bots/zulip_bots/bots/mention/test_mention.py", "zulip_bots/zulip_bots/bots/baremetrics/baremetrics.py", - "zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py" + "zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py", + "zulip_bots/zulip_bots/bots/salesforce/salesforce.py", + "zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py" ] parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.") diff --git a/zulip_bots/setup.py b/zulip_bots/setup.py index 7279bc3..2beccde 100755 --- a/zulip_bots/setup.py +++ b/zulip_bots/setup.py @@ -55,7 +55,8 @@ setuptools_info = dict( 'requests', # for bots/link_shortener and bots/jira 'python-chess[engine,gaviota]', # for bots/chess 'wit', # for bots/witai - 'apiai' # for bots/dialogflow + 'apiai', # for bots/dialogflow + 'simple_salesforce' # for bots/salesforce ], ) diff --git a/zulip_bots/zulip_bots/bots/salesforce/__init__.py b/zulip_bots/zulip_bots/bots/salesforce/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/salesforce/assets/link_details_example.png b/zulip_bots/zulip_bots/bots/salesforce/assets/link_details_example.png new file mode 100644 index 0000000000000000000000000000000000000000..ad1f2623ba142fdc7468551bf337bdf1e969422f GIT binary patch literal 16364 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 literal 0 HcmV?d00001 diff --git a/zulip_bots/zulip_bots/bots/salesforce/assets/query_example.png b/zulip_bots/zulip_bots/bots/salesforce/assets/query_example.png new file mode 100644 index 0000000000000000000000000000000000000000..1fff98cfcb677b22f3730f824826f41412488e4a GIT binary patch literal 32311 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 literal 0 HcmV?d00001 diff --git a/zulip_bots/zulip_bots/bots/salesforce/assets/top_opportunities_example.png b/zulip_bots/zulip_bots/bots/salesforce/assets/top_opportunities_example.png new file mode 100644 index 0000000000000000000000000000000000000000..9f8926c2d51e5ab56842f40bed9b93a2d3170026 GIT binary patch literal 15520 zcmcJ$cT|&I)F+C77z6}GrAd1gP+Cx$^d_iuMd=WcUP2X+5)g@Cp(#QrLO?`7dJ9NZ z5D_?S2_f^u_nSN4nsvW*XV%R9gPdfMXP>k8*=O(H{_T@jrpC9~PV=6o zqoZTHd*{}DIywe8@cs{o33$E8!aNHMCj#!@)}|}#yR-~^V06_o(xRiQOk_E7I0<}a z_Pt{jKu33`o%VO4%cszZj!sqn?k%kcPwm&InS(ENe5GzRi8m)Ty$TD!h$f}XAA|4S zxij%1s)Ep|pm6gX9M;7$F+m^2FcW{m(Se80bG&!yaNNuw_+enbn~NkLc8Bu8Z19Oi zU|?WkK;KcJeT3-Dt5Xnh+LGyh_uLj@1jc8=y8oMFpna6DvvWu4*c+d_JK0_NA&c#! z{y4Lt(Blj4YAT|?2D^qDRKj^+4>J0jgi5*WQx-Iks6e!p`wXS92GwM{#z~vsH=-{r zrovIE2@P)wL%tuws(gZNrqw7=8$ob1FFgf9#+3L#FCmCgmxqoYA=uvX(!G%>>^Rj{ zVM2TS+o4sX=1mX!T=q}})){I|jeVGjdBlgeDP|gX0?-nuc(y2od9}yK8RzI{KXW-d zue>k$keNcX!R8-+ldbY1`OR*08`|W6K;IBgMs;TLChOfV&lh$y zvsSv6w?zu9tE?YUR9&=qJIGh`i>$Uar5IYi&YA|tQ;id3I-p?X~da~f$hI%5+F#ZOF z(4J3@Ft|0=bU#;HNHt&~E{kVmFPHk}JcuHhJwq;FPYPMo2@5G;x+xXPp3}T$=DJf8 z&m8=#D?52%IuXO+>bieqh?ELdTF%S#0@bY79?yt*Y2@}3T_O6b(bL3~^ONemSBaO| z;La}t&=}U-)3Q;hTh)=hfBpBR8^=h}#n=YRx(f3;sJad}OO#A6YYY_ZAyo7ISF3fK zx||+?C;T@L*RdR{2gOeGNNu--*}oAuzlCzh2`_?D4IW1tosu%qyg04t~vQnsCz~zS(7)In#%@PxJg3m+!m}e5xxM;%Qc1A`;6`9`FFdi zGPt$bhFiI*MyI{s97zA?JV~3I79C?^m}J;oww$wO-}T+(@W?!{*P?_I*Dy{tb_M84 zdj^NKKPR<0JFN+8zThmMy7l6JUTYSz#|_F9%0}}txh^{8j|Q>FSXrkM2g~yhcUioN zo3EsX)O|;ow1zcr;j{`(I7dwjWnYB47qY{Zt0#CP(Wq*0ITZ+a30!e(Zc&0V2>IohGEIV#j7he@xQ^?@tp-8a{j{^B^2SS4kV9=yNGy0^YU+apBznY`_n##l4i=)nxI|!L0PIavQ-MbuPR(xT8{1sF6h;V6s2ZXZT|`;Rt@3r?LQd$ zxCDDK7*R`GEFF{VZyo0;3Bk2u!6WC79r%x^gRtXl{^XQA2!cK3a0dPzf%9XB4+fD2 z(zg_$S8HTnko?KzCU-oC-)u1FDLl#ua!56OgY!9HrHSM2{T($BTXpmNq>!5d$ZKI| z7(5ZiwC?14&L1?9(R~`xw1Evamv2Xhhz;ltEJuc>Y=s64k`kqr0Y}AAcOz zpP;$wzN&*O6O=n{#Uj;^?1;N#08z@Nkgc?AcKz?OWpuO%?p*$Bj$O|j&50qjvSOTp z=iD3A68Qwwa@at3@5aDw#k*v*4zc^ADE_ci6*gIDuTJVza5q6-UmmJ?!r8MW@R#R8r(&e5kCVqSz^Rf%j@*#Gf0}jo0{JXA7#CgtpM^VEdJc1rBZ! zU#Hx5!p~o`O3w&FM6DP))W0`_2(>zvzwA3h;u=1s?C6FRZNNM4?+(7`@DDP5gMt8O zvnzQh+%1|p+IwNJ1?#(m4vLg8EBFLkD#Q@@TE<7+>j!fJmIj0miC7!pjd?ve35^)|;bwd#>A;r0^ zJwPVG<@QF;eU9&wTTzQ9-ndsfexp@r9en2Q&S3n;(1?sqAwfQT%V^xi#e7m_Uw`NO zUc{~9VV?5#Sax)Gve-4Xi)xV2nZG)x13L2_ZG`Dt`#|et!%N3!5H6QMd*w0pWTaoN zuV1$v6A3sv{w6jBh}4Y75c|kAK8y0Gd+j;Qq0+3R2yLNDk!DVqnh@&cu*1SKLP}qd z`_IE;GU0!ogm})Fb3)*0XN)$v?J!ocA&j}UBy7n3k5%vah?@@DxbcmYbD}pVE%e}I zwmp8hiFf5b>m>S?;PgCc%-~ina$r%1Z;fRQ%3TvRsOJ0GBGhN9$gCjZ`F6_@Y+l@o zs%vMLh<~%^Kl<}!T$=xA^{ExCZO+HV(y@^=xuZ1Rx#0->GcQQqjb$#H)7xH(KBX;q z8WH$;vpMk-1%ntOd9~JP;E;XX9#_McFiS==9>|nf8W;`5lH;XuYI> zLiN;pA|h7e@7iJoBYwV)k>9yYL_up;%O{@}S_Zr2F~?g#lXP-_MX>?K(r~h|+_1D2 zn)1ylE32zoM?I0`s(!)(_q_NB1oylI3K-R_52_)=#2FpxlI`@+fbHr(u{~waWp?V2 zM6$rEYSuhEn3VWHP!)*Wk5SkewBKL^Oh{5romBdHj$ZG}o))eXMZ4mEelA9%>IhfI3Sd*>D%+?C zJMJ(`WSjw62Db9FR{9cTsQ)1!{?1g3cQZIJ%Cy~}GMGS!y-arY&{? zLGKy@^W>uj_IM`aZV6w;MBYH-qZ2n;QRsi(U2a z#2FaT{bSWsjV_olg1$9vFmE(SrtA*g58o6lxQlVD+Lpn9z@{qBE?dpC@2j zf)+|w)24z_C{BohvKkFz(1-Puner{L<$Vbe#Ct zCCEgKtqKlYf)Tp30!wpeAf ztNLnav!ZYHvgeW1zI}UX`PT>-l9y@kA?H?0?XG_GuioA`Fr+)c0&=;ZpBZ_39>rg_ z(XP_dFRS96?5Plmue*kAz`VoGw?BrN_Xb_I&=7-Q@a%Zh+ozD(fDvK>JFM}oS7g~z z+UvbZbzE>$D`5Jexi<#>CDsY5V&hk^eXIhYWYe4z;&4Ps=@({6QKSAT<-i;?UpvP8(wA!~3&mxmx->Nnzt85s2)ru!&xvuEdjvQLtkVW}A(O znsU0d;YYG6Dy!(*{*BD)U3)wCxSp>qV&+6ur=RgA z9E{RvYL59#qk`<*hJ_0slKRLGy|Yw4XN0PBN_UyA(KH4uZz`UFE36&zXppnDVIh=Vb_EV;tO8y_Y|T?lZf)62PoBfr2;H-0 ziyu8F&5@z7$OBqjSa6Dnm^qpj3AU})Z5hLtqkhVdd!?2KQet^aHj?a6)83q)yb;qV z*o5?P@!nTj+(_&OFJ?Q%+m*;ySG+z>p^y$eo#*@PfkCWFb zz2S)`8PQu-m4~S6TeX|bw|66HdSOSAcV^=>#S>Rn?(-UoZt=o? z?5m}Aj-?0b3c9QCL%#ZqM}GYfZ`75tOCWDM210`x`vZ1SHnu^e5K*`vAp#yR8}x^!Bu5)SFVu^Pm3sbv~ihfdR=DuiK0~ z9PZ}@jZ=YgLpIva?%>18wg~qMD_h(5`S~K`{U2kihA+wWmi82az}MlUfk20Q=)mox zEmNxB1J^M^sC`)@@#Nu{cd&ML>sSdm6)}(HlFLb1Crs^?O0Lgb?iX7nyibIY6ODwL zyjNejsp5KACJTJR)>&N0GJVs*889`lSX&jfadD%H^0Rilhov!(=+MkuHV&Om_6ioJ zcW@IVswn5~Sq*@^K+Nk?UpNxHiZK044OK0kGOtK|3~$x41Mk5^N=mDc2{7`OYt+EW z;+pbffHHX@0pq2+V({lM{N$hKh>}Gkl~?6DUksaM=f zT5BnHg-kq7Em)+Q@ABo~ZaYDf2UxaQ2o0G@3o>d0U(XTL;RLY@s9qH_$$lTyXtZ3p zbU3sVboehvMB&cREHvpI!ITvhT~lHB+}8Z^$cVafIJom^(V(9Xc>bBh@BpKB{;ZyB zlibo$v)_Bt$Hm+IoDLko|LUvQ9tzRQQ@ZycIBk&m^#AwRC&5uxQZkZCfhgd2x+26;(ykQtad+3vKudpex5QR^T5U837^_cKN>*1xmDHg0zYpH^ez2)rcQq0U z$9ISvQufptV;;BtCPXM*Hqk%{;Ub2sC``Q4flBb6H_-pY;_-~s#L3%2E?;Kj$=oiE zJl8d!7d-QJJm~1QA+gNE1U-MojC%Yje9nS~B>F|CZdyzTIh@|BCZ6jTG;Ty|2YcJ3Y8*}e zI(EMr?zj3;)wjQCN<$e85%ftx#F2Q~JSol}X{4nonOzsGMA`DPQ7D5{tP>8%A1WPh~&QAy9q)?-wP_Fqs>o($I zUskdO1SXGxOsj`p-(=10JG0W86Z4&~Ec_aiLd~;1du;k?v;^-G zs=3Z7+R?wekQv_E5TpkV2K)C2*a43!2EAE02;VwAsJ=#Q9kjvl#BuUEMSO8~WugWRm&CPJaR1_vz90*CU>dNy7-}p~ zA8el>;NFe)6FTZpI*exyjU*43sO}s*UpscgTynz3Xsr|0csPY1*6(Z#DRb<<(A}I; zRtgT4IG(mENu_V*GOs(+V}E>l_2>()z>a2`J!u)M*tteLbj}_A>?0+<%SkF-F95y3 z-+cXXE}J(w#?DyBjfaU6D)`T;>tzZhS`DG*KjFg+Y@kqnl%eacTzv+m5TRa%oU<4N=d zoghlV=L!DI<}`Be&rl$6f`HTd%vdgGc?}s;n=`Iih=461qM1U3Mq0O6b7NX{V$Wb; z?pYfU1Xy2)RW7mCD9KI!7tZao8JLGMS59GYtn#N&dw(L2WhD5J_7(eF>Q8V@Yn1wH z)v^*nV%DIY9j% zlpI}|0%1$kkBr*QaI^NzA*%*ocUMdGClw-@ijos(4P@wun&CoOWN$ZTVtrud82BgF zN3#1zw)5e}Ep&_)EG>@6M;Z)ctQBNK1mfrH(A&sB4I}4`2k;^f|#%A0Z^qK9h(+qkG9#) zlV9gjl@7Pxd~POeb_rsM6gck{gdw&lSt{gcHYDs?QH#B{PVS6OVeWmfTA=gigQLh+ zWXd3Rajo}p|7-HbmyVdyGg^}HVpX_^!wpxW{6%`(i$g=2Y5SR{evmXRn`2E0_eNUf zWfS;2qAC#De{3g&NpP1slZdmXyOz^pwAkzMUg;dBvO9+|q9;~95 zB*q2s6#XBpDyv@wBCJA%*pW0E&c$N0CWCox3!YXdANTz zwo{ekOAYd7Zy*T%<(#k1&9!ih=NGAhmI*7)qZKq6VwfofxCh-;wnx0Zf6&V9B3XVy zBMMv1&q;@CnCfm+gE7LfnW=3;mjzBb7Xm9fa%Atrn$~Zn>xK}Ebd@}{Z5#13{0Hsq z%I$S-uDiSZ(eaQE$q*{xb>^?s;gHcpHEtCt|?CbU_Q@X zXk-A$2(C=7E=(bXe~CUJ?yDRz!KXZ~j- zDPgbS@Ve!;DdE}_Ro6`5c(Lhd1L59`s*`WJ>XsGIB>;bNB<-1Gy5lr-UXpcT!GT>;@9CTAp1A?HM~UjGPUOx?5CGufo2Yq}Ee3Ijc=L})&o zIfhND^`XQ#ynI#%x-&BFaN<~@6|e?w@w;;)_pPV~^ww=w%l_M}0X;Nke{ zAQ#j29Hy_P3ky?*v&iF{68lrveRMvlzh9V2gpym@sQYBA!|_yIrSL*`!AE)6E*hT! z@W)Z~vl3wG(Nshi!U^4}iy_Xu>+MIBrw;rF3GK#jN1ajG(B0u>O7W)7V9`~_uTCEB zPkdZ%{Y#(%OtPuahb*ft*(aoNwTBN}>2aa3yT?Fc^NI)>?AyH7jM#h|*@p$h8RqnX zVhq&w%9&ch)ZzD;R3G!>kp3o^X`J~gdU+o70h~8CqnYkY9szsALn48xg z2nyxxv#vOQ4DBvLk+Yb?NZO&zr|#k8-5^?iWm0_O;7Rg%`77$A-%ac<{%sI`%V9bV z=N;O8v0Zl*GS)YOc}(w6ziTEa9a@&&X3V)b~Oca zj;3p|MZ5N=2~AWPokp4{IIp_wXE6WW7c6b{e{HV1ncq0IW1`qMd8+4V5V0Au6Qj$? zgcUvbx=H-gdJ%8WN!l&SV`14~&VRE|-gmSNIHcSRaa<{6`|U|3|3VSh2pI=|7m*O3 z*|oLT&Akug5f=0F!O)Nm%*n>^V^QZPP9sNz7Xg3vnzyTMh&?3Eh>noS7Kgf{wPS`G zZoxrh8)A1*;}cu&mJUguo8AbL|DR_^Q(okqaTD?di>r|z6iwuG(2G`j3&!K`M1%bO7JQHLs8ADZ50Q zh52li#kIwPUk$%LZzN$7`~LA5MBm7%iG|~ypu!_o0S*fmxb(9I*yKBS^KjL1gqhI(0*R=j>*X)YW97B1~SKV^UKiX@bePiQ)e%=X>CQ@afRERbvsHtXwbA&F>g-<$1NT){k}Ih8VTHfqN{gfIs*@7$Eo;8 zr{^e2j6>8e*B!Z=i)uhG?vs`o9_<$8BxfEez znf8fPawNY7^Nb!paoH3UW%Zc$;jMPTii9Qpu|Cz7uzz;!*teWrw!{A-{g7b49?`M6 zYOSu0GP;I4t}kbXr%)I%>_^`j9L7-oQZja+% zSE%MI$3}$6a5G`WRlWSL<-Ue7#=oLIt*-Vj1rhPGM^M^kaSHxo*oZYo@ku zb)&~n@)@jopJxH#GDe+6M|`#Q9B3ziyS6+=xx792LS&t{#^~d^Epnj-GD0;4sC;bk zjS`*Kb+D?k;nt$3wu6VK{tYCWh1CJd1_imB9i?ylx8cn0gJzn6E`+XX38L_g$b*!Y z4=qh5oBN6fnm310<+%q>K3-#=UYxmifGIHIJMGipHwLK9IMPDX?%{|`A(mBH$!$+X zCymN&UVN;%0p>7Ib(H^}>nK!0&h?%r6BIc_y9Ch0MG_mi`}wH&VxhmW1?Rp6W^8|Y zmU~>4qtmgnt&+bYa);3V#cS>d4%#JCq`YC4cwMFLYX)q8dpc#YZUJYw?URpgy;i(% z2_X4?e^e>gEX;Rq%V;t44Jl%c4(%>%0WLf-`5dJ*YRF9SS=!%Nq0gQ10TL_?7Q6>a zo%Ady`nF0cenw?+e$EbWrdQkF(Sc&iB0UbXQ>-`>RWYXqm)EclV`t>2za!!{dZelD zHyKIPD0|Zot~7dW?T1F+@K*5Q){KQv3H#YY|AK%Ar6$X1a}D!1C9p1C^ND$L`?GcI zUCS#f;Km&U_%@}eqCXsVgS)64uJCV6A4~;F;@D4)ehsJgwZr?o zx^>-y^02|KBQV8oCF4#zPLM>GRd62=PJFLt1q(I#KM7$Mmf_$U z5ErKLsr9!wK+(qg?DWx9vX6V81BdbVkgiZo8vhS#aqyO*bQldsOJD!X-O_R2|Gz%A z@d8&DO3nk5(A5udfIR=!-*l`D^bTx)*@I^ennER7e=A%WRuEjC#0F1Z)pK%kxnmPrCM=NE5Bbi>`oEO~j+_==Y6F4l-Y5j#5* zrPBC>!CHQK1KOMo!W?5uV2%ckA-A-FcP~B|yk-Y#PHncY&Idy|7J?OCmJ$gz*cZNO z1`I`PgTj)=ae61DS<0qY&Pm6Ke&4%)tPTiyWgY7VK$gcx7dHpNO z{a|-)+mT?7cu5M_uB1V!pJzdyyPP1MrA*X`D{Nqtw6#U+CG9x&XkocS4vXDIMB}M8 zf6U3zs~ite{riH0(D8VY*&1*2GTy$UwAc5Eb~y8{#RQe*R#4*f%8isgc`OaZ-vMmB zD*&1^O+Bf6ncY^pCPi4v3U#8);L++ejv(e|@=+DZ`j8;65MS1)KCn3FhNuyt9O}b0(P|y%pWAW4vcaV{9~w|Zaz_VJC0%FlQy$2|_h!Xc zu!T%byg|r_qY&i1ECE;5Fz)^7Qt7hCYbJq&8kB7o&bAOl!%;z%hKNc3qx>cZ(P}XK?t<)1Wrq zn=jUYpIp;=B)gV(?&ne3%F2+Vfun6f!^%NpA`c)>^R=dB9zqjDlZ{aMue|!-Hbu;d zzm}wnIsbn=4rII^e0y)Fm8A-t`GTr zJ-k@mG+S?HZ6JF{%~u!lsVzb@aq?6 z1jb)@Ww9JdL{EREySV2NJ@os8U#;d6eIff}>vtSM)?>Kx)%N2?M(xNocV;br`3Mb% z55kwSZztKA`B~OFuhWw!{cuJkA9i3KOFo_RAUp0$Y#Jp6ou94`B~@N)or0$Nve7%d zSl?*_c0)a{MQ3yQrfglPkYu77ONiYe3h^f{N2Bb3TrEtx&s-cu-S}(6dX5;8{cKk+?9%Rsok^)v&EBYdG|@( zPc(GNUes>CE7X`ja{-hY)qB~IDjOx`QB)n;h4-?nI+s9!dNOR!0N*2_^~fMNK9O3} za_*xSa*O@YOQ95WrXi`n=TPPzmzzhI$HCT%Eue9)Gf=2uJgz?C-1VN|ni7zTysWa2 zfc+94d02(WdJ}4w;2GalN>$jdNUt+#>0eVRqDUu4+F(Ky7oEwgUEtDmQ(*3+D=`Wj z9tV8xad*#Am8Wl~QRsrJI82#&+_|mfITp|nVOYqoV<-q{E&xc+YG6r`V?iXYW4)YJ zF&Ausvk@xMaFuuHOXwgJSCuBm6-XNv9fgZ_Euq#!ZMhyziL}BFyd}DlMvFi7HxPz= zk}uKrQlj-8ouW! zU$wQz2eOOAT<0r8dibsH_`Y8+SXJ7AN;s!H!jG>W7w7h^RzRm(R{iW=`jK~R-zP*> zY&5<9-0dBAG(vUmWHU@A(cJ%7ni^C_e|1Kv!lYjbG;o0$n;h zKE(phMc!}*A0{^FI7!nvp9_=S?udjYp83H^IU<4{$HxV8mOCBSb0Ik z&|+-1%v{h=fT??9k5aM9ZLuoH*U=BZ`;1?IB!Ls1QD=(+cF?FR3r3i2>`6_Y>m@-b zjdEm}6H2VP+df=0l{{b4JCM$_eY~}QHYmKO5Fc5;nh3S25*#Up!}agTPO%3_Vb)A5 z6R$pi=R88ITa{A)nzd)Wd`*n`yq>`6Hp}gxQD3w1vDl=KPF-g3WOasI@wP1R^z-}Q z2?glG%69=@MA(z~6Ff`1_yNWT<%)wI*ap);zROU+1{-0sOXgZN6F+a!gWr7v9L%4Tid>@x@+;0{xuvUL`+%PKg z2WEwt0L0Thpk?&`1)Q*w|6yy*x`EnPV;63#CD8Z9p_J7*Yjw&`tU5yX>6$sBsH7Zo z5fG^P99Zx^aQC80a)2k~w|{Oat9y49o39}@>o3j>z$(R1`8cr~2~5mw&OXRhDOsj2$E>BL^v8`&Yk1}+v6ZY z57f_Rtj|?&!CPwOI>qd4aG!@P1VHgm$qid4yLm{-lhAF=v3zMYR1MBJ zaCD0-%QyZ2NNBQLQB-djKk-AIGdl?V z7ZBv;k0;I2TaLuCU5h^TW0XFZCC3|yy5c~nDQSg_$5Mis;k8{e8YE5as zeA+Ur%`AvjT8`-3?S{%yIa6&H&bk#O98UT;=SzP58Y<{=4Shcw=cRgh-I;tfA+Kr% zNyrFz*O!xuGdpY+NbE!~?e9MQ1EpC6joxHx144$A?DPL8y@&ify?0mW0jmF|+rhK< z@ElXZ>T9|s1~WUM68_@1FFx+D!0%z_Z*E5za?5?!FBX@DD`yvGu|W7cljH5WOfNxh z60ETE2c9T!*VOFP!d?sYC`{FXG?R_=1l0X~<9c|M@8c1ij%7fITLBpa0E&)najlO4 zJK@~gOrtbXp5Y%7Ii0>QAJIFEM4tP${m5cPZboRmE=wPsip*Y9{63zAJ?=Tx2P%$mENut!#$ivQHJk;)c zQSbCbhoaQeZ!wvuL42&lXX5W&y>mW6%0{~N)MXosM#9ll&}sNW_<5YwG_B9`4E=MH zv8~8G9mW3c8crt-A~3miEzcYzm3zy<^kG|+5AGTVuk;o7?&y0~+nZ;J&GzI~4p0gf zf-w=W-hOtm^FFg8g;5)alHA%N<>YrHs928$aZZ|G)cH+9?8V8~VbK0Q-S9OO%*fZE z-{iQ~;BfMvzxkCGg`P>Aj~aL!M60j9Z3_R5rr(nO z`7g(@n@=|%(pM%N&ds#i`BlN*m@t@0=FAs4eiJsEBq;1Mso~+F`#h@-H z14qNQw?>JIJ$t(DKPTS%$k3{2^~t`K`&|2($S&`|Yr%14>xEXd)HXn?{UQO5J4vq{ zo~lY`rytI}D#9@-Q!o9a$eqXY$(hFrB^SD~V)|;3(t(_L_nx7jd)77FQto?VOIz&< zEp=T0={%~8%uBz~9wWf1FIvr0)+cK(JpymIVaSuBx!88kZH~k`_Y;-qy5QwECyDYa zRUTHKt33aNb?~}p<4rh*LOO07Sq^#KAI*=zK>+bhUP@1=&vq;Ml9b$%fGm{_ zr?K+?-RfB^sId^I%|oQ;*WtZ1tZ4};Sd?1TV1{R{+-Oi|{_r>CKavN6zuKub$shRK z5~OC@c#+ez$vM!p`ijdbXrz@)wPRt{$u)Be&Z&2r6GhTyRwhC;uQ1mUDjD``4ZB2qK2Vq1 zdGN_|75jkH!;pK{2cp>Wt+d}j)p%U=4B})fq|r#=XTwN-S1J)(RHhsu=maeipn=_I z=K=Qt|6B7)2mcrO@PD|lFl`a6*Z!h}|9TX8OJ#2z+tT^R`sr)h19x?eZxk literal 0 HcmV?d00001 diff --git a/zulip_bots/zulip_bots/bots/salesforce/doc.md b/zulip_bots/zulip_bots/bots/salesforce/doc.md new file mode 100644 index 0000000..a71d7c3 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/doc.md @@ -0,0 +1,80 @@ +# Salesforce bot + +The Salesforce bot can get records from your Salesforce database. +It can also show details about any Salesforce links that you post. + +## Setup + +1. Create a user in Salesforce that the bot can use to access Salesforce. +Make sure it has the appropriate permissions to access records. +2. In `salesforce.conf` paste the Salesforce `username`, `password` and +`security_token`. +3. Run the bot as explained [here](https://zulipchat.com/api/running-bots#running-a-bot) + +## Examples + +### Standard query +![Standard query](assets/query_example.png) + +### Custom query +![Custom query](assets/top_opportunities_example.png) + +### Link details +![Link details](assets/link_details_example.png) + +## Optional Configuration (Advanced) + +The bot has been designed to be able to configure custom commands and objects. + +If you wanted to find a custom object type, or an object type not included with the bot, +like `Event`, you can add these by adding to the Commands and Object Types in `utils.py`. + +A Command is a phrase that the User asks the bot. For example `find contact bob`. To make a Command, +the corresponding object type must be made. + +Object types are Salesforce objects, like `Event`, and are used to tell the bot which fields of the object the bot +should ask for and display. + +To show details about a link posted, only the Object Type for the object needs to be present. + +Please read the +[SOQL reference](https://goo.gl/6VwBV3) +to make custom queries, and the [simple_salesforce documentation](https://pypi.python.org/pypi/simple-salesforce) +to make custom callbacks. + +### Commands + +For example: "find contact tim" + +In `utils.py`, the commands are stored in the list `commands`. + +Parameter | Required? | Type | Description | Default +--------- | --------- | ---- | ----------- | ------- +commands | [x] | list[str] | What the user should start their command with | `None` +object | [x] | str | The Salesforce object type in `object_types` | `None` +query | [ ] | str | The SOQL query to access this object* | `'SELECT {} FROM {} WHERE Name LIKE %\'{}\'% LIMIT {}'` +description | [x] | str | What does the command do? | `None` +template | [x] | str | Example of the command | `None` +rank_output | [ ] | boolean | Should the output be ranked? (1., 2., 3. etc.) | `False` +force_keys | [ ] | list[str] | Values which should always be shown in the output | `[]` +callback | [ ] | callable** | Custom handling behaviour | `None` + +**Note**: *`query` must have `LIMIT {}` at the end, and the 4 parameters are `fields`, `table` (from `object_types`), +`args` (the search term), `limit` (the maximum number of terms) + +**`callback` must be a function which accepts `args: str`(arguments passed in by the user, including search term), +`sf: simple_salesforce.api.Salesforce` (the Salesforce handler object, `self.sf`), `command: Dict[str, Any]` +(the command used from `commands`) + +### Object Types +In `utils.py` the object types are stored in the dictionary `object_types`. + +The name of each object type corresponds to the `object` referenced in `commands`. + +Parameter | Required? | Type | Description +--------- | --------- | ---- | ----------- +fields* | [x] | str | The Salesforce fields to fetch from the database. +name | [x] | str | The API name of the object**. + +**Note**: * This must contain Name and Id, however Id is not displayed. +** Found in the salesforce object manager. diff --git a/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_multiple_results.json b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_multiple_results.json new file mode 100644 index 0000000..b364418 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_multiple_results.json @@ -0,0 +1,26 @@ +{ + "response": { + "totalSize": 2, + "done": true, + "records": [ + { + "attributes": { + "type": "Contact", + "url": "" + }, + "Id": "foo_id", + "Name": "foo", + "Phone": "020 1234 5678" + }, + { + "attributes": { + "type": "Contact", + "url": "" + }, + "Id": "bar_id", + "Name": "bar", + "Phone": "020 5678 1234" + } + ] + } +} diff --git a/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_no_results.json b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_no_results.json new file mode 100644 index 0000000..d7b0714 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_no_results.json @@ -0,0 +1,8 @@ +{ + "response": { + "totalSize": 0, + "done": true, + "records": [ + ] + } +} diff --git a/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_one_result.json b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_one_result.json new file mode 100644 index 0000000..f6453fc --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_one_result.json @@ -0,0 +1,17 @@ +{ + "response": { + "totalSize": 1, + "done": true, + "records": [ + { + "attributes": { + "type": "Contact", + "url": "" + }, + "Id": "foo_id", + "Name": "foo", + "Phone": "020 1234 5678" + } + ] + } +} diff --git a/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_top_opportunities.json b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_top_opportunities.json new file mode 100644 index 0000000..d5fe7be --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/fixtures/test_top_opportunities.json @@ -0,0 +1,26 @@ +{ + "response": { + "totalSize": 2, + "done": true, + "records": [ + { + "attributes": { + "type": "Opportunity", + "url": "" + }, + "Id": "foo_id", + "Name": "foo", + "Amount": 2 + }, + { + "attributes": { + "type": "Opportunity", + "url": "" + }, + "Id": "bar_id", + "Name": "bar", + "Amount": 1 + } + ] + } +} diff --git a/zulip_bots/zulip_bots/bots/salesforce/requirements.txt b/zulip_bots/zulip_bots/bots/salesforce/requirements.txt new file mode 100644 index 0000000..91cdb7f --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/requirements.txt @@ -0,0 +1 @@ +simple_salesforce diff --git a/zulip_bots/zulip_bots/bots/salesforce/salesforce.conf b/zulip_bots/zulip_bots/bots/salesforce/salesforce.conf new file mode 100644 index 0000000..ca7b606 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/salesforce.conf @@ -0,0 +1,4 @@ +[salesforce] +username=foo +security_token=bar +password=baz diff --git a/zulip_bots/zulip_bots/bots/salesforce/salesforce.py b/zulip_bots/zulip_bots/bots/salesforce/salesforce.py new file mode 100644 index 0000000..da42f65 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/salesforce.py @@ -0,0 +1,178 @@ +# See readme.md for instructions on running this code. + +from typing import Any +import simple_salesforce +from typing import Dict, Any, List +import getpass +import re +import logging +import json +from zulip_bots.bots.salesforce.utils import * + +base_help_text = '''Salesforce bot +This bot can do simple salesforce query requests +**All commands must be @-mentioned to the bot.** +Commands: +{} +Arguments: +**-limit **: the maximum number of entries sent (default: 5) +**-show**: show all the properties of each entry (default: false) + +This bot can also show details about any Salesforce links sent to it. + +Supported Object types: +These are the types of Salesforce object supported by this bot. +The bot cannot show the details of any other object types. +{}''' + +login_url = 'https://login.salesforce.com/' + + +def get_help_text() -> str: + command_text = '' + for command in commands: + if 'template' in command.keys() and 'description' in command.keys(): + command_text += '**{}**: {}\n'.format('{} [arguments]'.format( + command['template']), command['description']) + object_type_text = '' + for object_type in object_types.values(): + object_type_text += '{}\n'.format(object_type['table']) + return base_help_text.format(command_text, object_type_text) + + +def format_result( + result: Dict[str, Any], + exclude_keys: List[str]=[], + force_keys: List[str]=[], + rank_output: bool=False, + show_all_keys: bool=False +) -> str: + exclude_keys += ['Name', 'attributes', 'Id'] + output = '' + if result['totalSize'] == 0: + return 'No records found.' + if result['totalSize'] == 1: + record = result['records'][0] + output += '**[{}]({}{})**\n'.format(record['Name'], + login_url, record['Id']) + for key, value in record.items(): + if key not in exclude_keys: + output += '>**{}**: {}\n'.format(key, value) + else: + for i, record in enumerate(result['records']): + if rank_output: + output += '{}) '.format(i + 1) + output += '**[{}]({}{})**\n'.format(record['Name'], + login_url, record['Id']) + added_keys = False + for key, value in record.items(): + if key in force_keys or (show_all_keys and key not in exclude_keys): + added_keys = True + output += '>**{}**: {}\n'.format(key, value) + if added_keys: + output += '\n' + return output + + +def query_salesforce(arg: str, sf: Any, command: Dict[str, Any]) -> str: + arg = arg.strip() + qarg = arg.split(' -', 1)[0] + split_args = [] # type: List[str] + raw_arg = '' + if len(arg.split(' -', 1)) > 1: + raw_arg = ' -' + arg.split(' -', 1)[1] + split_args = raw_arg.split(' -') + limit_num = 5 + re_limit = re.compile('-limit \d+') + limit = re_limit.search(raw_arg) + if limit: + limit_num = int(limit.group().rsplit(' ', 1)[1]) + logging.info('Searching with limit {}'.format(limit_num)) + query = default_query + if 'query' in command.keys(): + query = command['query'] + object_type = object_types[command['object']] + res = sf.query(query.format( + object_type['fields'], object_type['table'], qarg, limit_num)) + exclude_keys = [] # type: List[str] + if 'exclude_keys' in command.keys(): + exclude_keys = command['exclude_keys'] + force_keys = [] # type: List[str] + if 'force_keys' in command.keys(): + force_keys = command['force_keys'] + rank_output = False + if 'rank_output' in command.keys(): + rank_output = command['rank_output'] + show_all_keys = 'show' in split_args + if 'show_all_keys' in command.keys(): + show_all_keys = command['show_all_keys'] or 'show' in split_args + return format_result(res, exclude_keys=exclude_keys, force_keys=force_keys, rank_output=rank_output, show_all_keys=show_all_keys) + + +def get_salesforce_link_details(link: str, sf: Any) -> str: + re_id = re.compile('/[A-Za-z0-9]{18}') + re_id_res = re_id.search(link) + if re_id_res is None: + return 'Invalid salesforce link' + id = re_id_res.group().strip('/') + for object_type in object_types.values(): + res = sf.query(link_query.format( + object_type['fields'], object_type['table'], id)) + if res['totalSize'] == 1: + return format_result(res) + return 'No object found. Make sure it is of the supported types. Type `help` for more info.' + + +class SalesforceHandler(object): + def usage(self) -> str: + return ''' + This is a Salesforce bot, which can search for Contacts, + Accounts and Opportunities. And can be configured for any + other object types. + + It will also show details of any Salesforce links posted. + + @-mention the bot with 'help' to see available commands. + ''' + + def get_salesforce_response(self, content: str) -> str: + content = content.strip() + if content is None or content == 'help': + return get_help_text() + if content.startswith('http') and 'force' in content: + return get_salesforce_link_details(content, self.sf) + for command in commands: + for command_keyword in command['commands']: + if content.startswith(command_keyword): + args = content.replace(command_keyword, '').strip() + if args is not None and args != '': + if 'callback' in command.keys(): + return command['callback'](args, self.sf, command) + else: + return query_salesforce(args, self.sf, command) + else: + return 'Usage: {} [arguments]'.format(command['template']) + return get_help_text() + + def initialize(self, bot_handler: Any) -> None: + self.config_info = bot_handler.get_config_info('salesforce') + try: + self.sf = simple_salesforce.Salesforce( + username=self.config_info['username'], + password=self.config_info['password'], + security_token=self.config_info['security_token'] + ) + except simple_salesforce.exceptions.SalesforceAuthenticationFailed as err: + logging.error( + 'Failed to log in to Salesforce. {} {}'.format(err.code, err.message)) + quit() + + def handle_message(self, message: Any, bot_handler: Any) -> None: + try: + bot_response = self.get_salesforce_response(message['content']) + bot_handler.send_reply(message, bot_response) + except Exception as e: + bot_handler.send_reply('Error. {}.'.format(e), bot_response) + + +handler_class = SalesforceHandler diff --git a/zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py b/zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py new file mode 100644 index 0000000..7f8ac14 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py @@ -0,0 +1,201 @@ +from zulip_bots.test_lib import BotTestCase, read_bot_fixture_data +import simple_salesforce +from simple_salesforce.exceptions import SalesforceAuthenticationFailed +from contextlib import contextmanager +from unittest.mock import patch +from typing import Any, Dict +import logging + + +@contextmanager +def mock_salesforce_query(test_name: str, bot_name: str) -> Any: + response_data = read_bot_fixture_data(bot_name, test_name) + sf_response = response_data.get('response') + + with patch('simple_salesforce.api.Salesforce.query') as mock_query: + mock_query.return_value = sf_response + yield + + +@contextmanager +def mock_salesforce_auth(is_success: bool) -> Any: + if is_success: + with patch('simple_salesforce.api.Salesforce.__init__') as mock_sf_init: + mock_sf_init.return_value = None + yield + else: + with patch( + 'simple_salesforce.api.Salesforce.__init__', + side_effect=SalesforceAuthenticationFailed(403, 'auth failed') + ) as mock_sf_init: + mock_sf_init.return_value = None + yield + + +@contextmanager +def mock_salesforce_commands_types() -> Any: + with patch('zulip_bots.bots.salesforce.utils.commands', mock_commands), \ + patch('zulip_bots.bots.salesforce.utils.object_types', mock_object_types): + yield + + +mock_config = { + 'username': 'name@example.com', + 'password': 'foo', + 'security_token': 'abcdefg' +} + +help_text = '''Salesforce bot +This bot can do simple salesforce query requests +**All commands must be @-mentioned to the bot.** +Commands: +**find contact [arguments]**: finds contacts +**find top opportunities [arguments]**: finds opportunities + +Arguments: +**-limit **: the maximum number of entries sent (default: 5) +**-show**: show all the properties of each entry (default: false) + +This bot can also show details about any Salesforce links sent to it. + +Supported Object types: +These are the types of Salesforce object supported by this bot. +The bot cannot show the details of any other object types. +Table +Table +''' + + +def echo(arg: str, sf: Any, command: Dict[str, Any]) -> str: + return arg + + +mock_commands = [ + { + 'commands': ['find contact'], + 'object': 'contact', + 'description': 'finds contacts', + 'template': 'find contact ', + }, + { + 'commands': ['find top opportunities'], + 'object': 'opportunity', + 'query': 'SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}', + 'description': 'finds opportunities', + 'template': 'find top opportunities ', + 'rank_output': True, + 'force_keys': ['Amount'], + 'exclude_keys': ['Status'], + 'show_all_keys': True + }, + { + 'commands': ['echo'], + 'callback': echo + } +] + + +mock_object_types = { + 'contact': { + 'fields': 'Id, Name, Phone', + 'table': 'Table' + }, + 'opportunity': { + 'fields': 'Id, Name, Amount, Status', + 'table': 'Table' + } +} + + +class TestSalesforceBot(BotTestCase): + bot_name = "salesforce" # type: str + + def _test(self, test_name: str, message: str, response: str, auth_success: bool=True) -> None: + with self.mock_config_info(mock_config), \ + mock_salesforce_auth(auth_success), \ + mock_salesforce_query(test_name, 'salesforce'), \ + mock_salesforce_commands_types(): + self.verify_reply(message, response) + + def _test_initialize(self, auth_success: bool=True) -> None: + with self.mock_config_info(mock_config), \ + mock_salesforce_auth(auth_success), \ + mock_salesforce_commands_types(): + bot, bot_handler = self._get_handlers() + + def test_bot_responds_to_empty_message(self) -> None: + self._test('test_one_result', '', help_text) + + def test_one_result(self) -> None: + res = '''**[foo](https://login.salesforce.com/foo_id)** +>**Phone**: 020 1234 5678 +''' + self._test('test_one_result', 'find contact foo', res) + + def test_multiple_results(self) -> None: + res = '**[foo](https://login.salesforce.com/foo_id)**\n**[bar](https://login.salesforce.com/bar_id)**\n' + self._test('test_multiple_results', 'find contact foo', res) + + def test_arg_show(self) -> None: + res = '''**[foo](https://login.salesforce.com/foo_id)** +>**Phone**: 020 1234 5678 + +**[bar](https://login.salesforce.com/bar_id)** +>**Phone**: 020 5678 1234 + +''' + self._test('test_multiple_results', 'find contact foo -show', res) + + def test_no_results(self) -> None: + self._test('test_no_results', 'find contact foo', 'No records found.') + + def test_rank_and_force_keys(self) -> None: + res = '''1) **[foo](https://login.salesforce.com/foo_id)** +>**Amount**: 2 + +2) **[bar](https://login.salesforce.com/bar_id)** +>**Amount**: 1 + +''' + self._test('test_top_opportunities', 'find top opportunities 2', res) + + def test_limit_arg(self) -> None: + res = '''**[foo](https://login.salesforce.com/foo_id)** +>**Phone**: 020 1234 5678 +''' + with self.assertLogs(level='INFO') as log: + self._test('test_one_result', 'find contact foo -limit 1', res) + self.assertIn('INFO:root:Searching with limit 1', log.output) + + def test_help(self) -> None: + self._test('test_one_result', 'help', help_text) + self._test('test_one_result', 'foo bar baz', help_text) + self._test('test_one_result', 'find contact', + 'Usage: find contact [arguments]') + + def test_bad_auth(self) -> None: + with self.assertLogs(level='ERROR') as log, \ + patch('builtins.quit'): + self._test_initialize(auth_success=False) + self.assertIn( + 'ERROR:root:Failed to log in to Salesforce. 403 auth failed', log.output) + + def test_callback(self) -> None: + self._test('test_one_result', 'echo hello', 'hello') + + def test_link_normal(self) -> None: + res = '''**[foo](https://login.salesforce.com/foo_id)** +>**Phone**: 020 1234 5678 +''' + self._test('test_one_result', + 'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res) + + def test_link_invalid(self) -> None: + self._test('test_one_result', + 'https://login.salesforce.com/foo/bar/1c3e5g7$i9k1m3o5q7', + 'Invalid salesforce link') + + def test_link_no_results(self) -> None: + res = 'No object found. Make sure it is of the supported types. Type `help` for more info.' + self._test('test_no_results', + 'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res) diff --git a/zulip_bots/zulip_bots/bots/salesforce/utils.py b/zulip_bots/zulip_bots/bots/salesforce/utils.py new file mode 100644 index 0000000..89bf4a1 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/salesforce/utils.py @@ -0,0 +1,47 @@ +link_query = 'SELECT {} FROM {} WHERE Id=\'{}\'' +default_query = 'SELECT {} FROM {} WHERE Name LIKE \'%{}%\' LIMIT {}' + +commands = [ + { + 'commands': ['search account', 'find account', 'search accounts', 'find accounts'], + 'object': 'account', + 'description': 'Returns a list of accounts of the name specified', + 'template': 'search account ' + }, + { + 'commands': ['search contact', 'find contact', 'search contacts', 'find contacts'], + 'object': 'contact', + 'description': 'Returns a list of contacts of the name specified', + 'template': 'search contact ' + }, + { + 'commands': ['search opportunity', 'find opportunity', 'search opportunities', 'find opportunities'], + 'object': 'opportunity', + 'description': 'Returns a list of opportunities of the name specified', + 'template': 'search opportunity ' + }, + { + 'commands': ['search top opportunity', 'find top opportunity', 'search top opportunities', 'find top opportunities'], + 'object': 'opportunity', + 'query': 'SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}', + 'description': 'Returns a list of opportunities organised by amount', + 'template': 'search top opportunities ', + 'rank_output': True, + 'force_keys': ['Amount'] + } +] # type: List[Dict[str, Any]] + +object_types = { + 'account': { + 'fields': 'Id, Name, Phone, BillingStreet, BillingCity, BillingState', + 'table': 'Account' + }, + 'contact': { + 'fields': 'Id, Name, Phone, MobilePhone, Email', + 'table': 'Contact' + }, + 'opportunity': { + 'fields': 'Id, Name, Amount, Probability, StageName, CloseDate', + 'table': 'Opportunity' + } +} # type: Dict[str, Dict[str, str]]