From 505083a92166c9fa439fbbf43fa0860a69d2dd46 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Sun, 26 Dec 2021 09:34:49 +0100 Subject: [PATCH 01/15] Added hours reporting to the summary --- src/Clockify/Reports/ReportUtil.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Clockify/Reports/ReportUtil.cs b/src/Clockify/Reports/ReportUtil.cs index 6f486c1..3fb65c9 100644 --- a/src/Clockify/Reports/ReportUtil.cs +++ b/src/Clockify/Reports/ReportUtil.cs @@ -3,6 +3,7 @@ using System.Text; using Bot.Clockify.Models; using Microsoft.Bot.Connector; +using Microsoft.Recognizers.Text; namespace Bot.Clockify.Reports { @@ -50,7 +51,7 @@ public static string SummaryForReportEntries(string channel, IEnumerable Date: Sun, 26 Dec 2021 20:32:12 +0100 Subject: [PATCH 02/15] improved README.md --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ images/img1.JPG | Bin 0 -> 44831 bytes images/img2.jpg | Bin 0 -> 59799 bytes images/img3.jpg | Bin 0 -> 58768 bytes images/img4.jpg | Bin 0 -> 34696 bytes 5 files changed, 46 insertions(+) create mode 100644 images/img1.JPG create mode 100644 images/img2.jpg create mode 100644 images/img3.jpg create mode 100644 images/img4.jpg diff --git a/README.md b/README.md index aa6ac66..91b8d53 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,52 @@ Make sure to provide required configuration values. The app currently needs a va "MicrosoftAppPassword": "" } ``` +Description: +* LuisAppID: AppId for the app you created within luis.ai +* LuisAPIKey: API Key you set for your LUIS service under portal.azure.com +* LuisAPIHostName: Hostname from portal.azure.com without https://! +* ProactiveBotApiKey: The API Key you need to trigger https://:3978/api/timesheet/remind (pass the API-Key as header info "ProactiveBotApiKey") +* MicrosoftAppId: The AppId of your WebApplication you get from portal.azure.com +* MicrosoftAppPassword: The password for your application. You also get this from portal.azure.com +* KeyVaultName: the name of your Vault storage for storing the tokens + +Important: if you test the bot locally, you should use a reduced set of settings: + +```json +{ + "LuisAppId": "", + "LuisAPIKey": "", + "LuisAPIHostName": "", + "ProactiveBotApiKey": "" +} +``` +You still need the LUIS service to be active. + +###LUIS +For proper operation, you must provide a LUIS model. This can be done at luis.ai + +####Intents +You must create different intents. The intents below are the ones that i've figured out to be the minimum. +Add also @datetimeV2 as a feature. + +![images/img1.JPG](images/img1.JPG) + +![images/img2.jpg](images/img2.jpg) +![images/img3.jpg](images/img3.jpg) + +####Entities +You need also at least one additional entity called "WorkedEntity". This stores the project you have worked on. + +![images/img4.jpg](images/img4.jpg) + +###Auto reminder + +The auto reminder is triggered by an endpoint. You have to call [GET] http://localhost:3978/api/timesheet/remind and pass ProactiveBotApiKey within the header and pass as value the "ProactiveBotApiKey" value. + +### Clockify +The first time you contact the bot, he will ask you for your clockify API-Key and stores it within the KeyVault. + +###Run Then run the bot. For example, from a terminal: diff --git a/images/img1.JPG b/images/img1.JPG new file mode 100644 index 0000000000000000000000000000000000000000..ec11cae88481c18a9a91223283796f2a3a0720d0 GIT binary patch literal 44831 zcmeFa1za3mwl7))O>lyH@ZheEBm@gVgG+E}pmBHkfIyG{0fG}GI0X0L?oMbNg1bAn z^Ua)@dFS3aZ)VPS-#zDDir? ztSbpuuqgn@$pMT20H6Q}a6-T%SPcud^Ewy|Ai(Nyu)0=q%CB#L5_W2IixfbH9Ycfd zi@*v%x&`|{`Bi(^9uRmy-~oXL1RfB0K;ZvI1jMZjoQ%OXl+q@~U^5eY06_c*|GP$k z3)38OKO+34-Vh!2dmWya9{{4G|62d6c7c-o3&98N;{QfIdZ76Mfd>R05O_e~0fE0F zz{$?ZA;`fg$jM8|!7j)Js|W!9t78Bl4p;#OfD>R0fB_qr*+LpH`Mqui`-W*HFxv+z zmARv%tsonlwF9exv5lb#tC0Mgqq4q9W)|K(2zW5L<|eqXDHW#LC)1&{de~ z_ud6z^j@7UmY44+Bi81Q#m<-jRj2&OnD5Ajd@rM4Y-V1 zI5>6dP~6_cz|rIttcd(tW)==k7EWH3e_MK1UOoX3+rMiE zV*3Tg-%Rw6o%uh6mI%n$NYLol+->atH2hl=yZ_v3TY*LXoof9jLSe`MRDq2EW536b zAA{KbcJTihjo**JxDWQbzwj#Tmz4h>Y!8F{Qy@RU^#IpDg}^`6_<_3~;QFT!_@^2_ zaMyn`xc;i|npne>To;%&dk=-F(a4C1NQel?NQg)%$jB&9uu)+;#uGeDEHrFFJYph3 zJVF8za{6Z^WVB=igjB3lv<%Ogn3;$v*|^vkIq4ah7=LvFhm3;qQbJP3 ze|o!b2Cz{cJw>>Nhob@>VZ*^=!`-(76foZ;BFvll%c1$_4ek*<0wNMJ%Ht=fum+V_ zz#}+#_(urvh=>R^y*gjfnG%;|0=FB?DwCJ6uk`s4pngFH2kSlt&I|xD4(6 zA3wn-AS5EDrK4wf&dANf%f~MuDE>-9Qc7Cp^;;EHHFXV5EhA$SQ!{h0g@dD$vx}>n zd%$mjLBa1sLZf3o#m2>dPDsqi%*y_nlbe@cR$ftARb5kC*V@+J(b?7A(>ppgJ~25p z{bOcnd1dwI+WN-k*5T3d$?4ho#pTs6y5Io#Kauqp%KjZ)*f6>tAt1mbApfEZ?vV?u zz+)pIKI1^bd7*@CVE2@Y(+>srWz?6_mdDgw$_IFc_9IX5X}FhY4}X#NJ7xbEVgCOT zWq%>;uXN1-Xz*~b#e>HNM1d>GP=@qJ59L3HLCp}sjpB$W-ZF$jd;((gvvy*d852Ux z`JDCX8uMa=g1%*WNy?q5Pml41&pn`KmLWJE=JCz_rSeW>215zs#n_>O>IK+g z=^ogrw|xF(-1w7DQtsRjRWu(5OE!LoDdB?DLxqloknhCRk?rcTMcF!$8 zhM&8{`pm`bB|i;_nV?H->UK7(1BKvP9dTwb^l^~_B9(Ztr(d(ptF@fQ`JDHpHPb%| z@?v11EH3(sJ~^WJFi$;IxgUUS@g1kEJV|`~4E4#zyQs5!j{x#W$Jv(((CN?e(OtWV25!1CB;?t`v^V@8Aa9sX2! z1Gr#8>L8|#mv81>+OtqO@wXrI`xkc5HzVI=tst!M7k^la>>!CG(%nj=cwbSPbrC5| zqpMAALJIHSHy%cg(zAs^50X1+ioFweCy$iyyk1<0puhq_%yKn1MDWH=$qXOEsYg2U z4ohr5a}zP8_x%$5(nDS8plf-ua#?}U;vME;Rk%&|c9rxf9OPpdZdfrYA3vo(p}(m5 zojUT@+a33?y4Wy+;U98tHe_?1Z+Q0^Lui_NW@>wy5f0TXSNpE-f%ofYk!7{14)ilrjOOd(LA(W ztKf6{!VHcd1kOeE#}m=+P%gjDwJw?%+1w$qj2hWS4sY^>=>>CTS^NM8C$j9l&qoBI zm^@Wd+|MHS4tBnL^33A3MOm-U%x)|4e-H?NUm$#UxwyRtXk%bk>aBC+J@6Xm`}<-S zy#s=v8>Pa{tK#~vxB2IC`jcgsNAz*xNTf`F8*qe23rf3-r)KvQ?tE)Sc+K&pgQ zc9W4ZE{;#>?13nh$7pe|45%)$C847!}Agt-}#8k3f&fFsMf^#qK_k* zGK#`!$&Y>7uV`Al2NJLwj8JVTE+|$#cMk$&zI`8Kt7w#6mMmY~m7LiO0FUV1#_=sa zYQ}-H@<-WjF44U(S=2)Zlk-nhHWGAJlQWFp^{vi>$_p>%oBc=8el|IUnNc4Vm$dsl z_C#MEkf}s>X&Y8>UhpXqi;jx-fekpb)Bz76gimA>$Lm z83;=@K1`H9`mk^5?K>q=IYe^%$@i}FP@uV|;r-Kl0M0@VWOVjp-0mI_NxQa)-4Ve( z;0yM|Mjsa;82jEDZ%y;r?MSB4w^>w()M@vXH{!wSJKmU%o*X0HSYF|UGS;gH-kbArKP2N-6FlvO6Pn<|Nh~Qs1 zEr}w^(4-QLu?50Xw#(5SM z^s-yTm>z0NRBXSlUUs+;WHP*7|3PXBNBZ5dLcKn%UuG=XcOCz3=bK^?WfY#dN$_XISJue5RYn3357o-Uui0ixLzw;X!{Tn}gLy7+3+QDpw<#lC z7rO`Zc>_N3J&oX+9p{a6p2W|C7}?-eHW)p|Wx&RD2l(-LNSKhd196qa5Fg6l!Qidz zH_@%>>s6m{Krqzw>>fy!yNo={^za#?yXm?ZhTr&VV{^H>@cAAf=BvIrvgwC46nSX) z0M7$cJX{J7lHx%={>@$C!Ibcam{MwQE$^f-(S4@0BX4mm_Hr_Oz&gxLGw$JEC!l4M zD~74=Q4tMO90jg6eTR~M1ZRWAcaAaP`%ZChUMck2b-GhnY`1k1)W=JW^(21|kz-;w z)%6^iQ_`??HR`E}(A#Y^l?x*=j(1aQ*^m^#tJOJD(nGBsA)r5GB20{YPaCO0Eb_sS z2mO^K-Dl9e3U9YOZuf;Ds{XF38a?TvgW5@bikgqGQ;OhxPgHQ+-SSJcR1 z*gAJ1oSRKJAaN7#JDr(?psnuOt~BCy>eu>xd%16<2k)#H$g57_+7JbkYc3}Z0- zLiqUwbRx4~Zm!#O+y$O`>;3d;-`ObPo2+OT5h)vnRLD`S$Og-Uce(p&zYkfBCJ;(5 z;nS--A=pL0KbyZ6 z88_m`ay2R%%4o|P%ESjNg@hxx&d0mDbhSCihqn;{Na0zto3`MumcpDzHsw(6uaX!o zu&J5o{64k7{C7S-ZTElPISZ4Au6tsCT>uNBRk<8N1*KdVszPU$afayts8!DeuiYd_pu6v+Jov`CZzWueb-jf_6}(CkSJ^-gua8>pCao_?SW~^sMUZ zXQ9<|vW>xHa9#3Do^SN?2XcxQ)(nC^&GF}uF*i~DbkL#*%baXb$lYWzHHetTuc|%f zL#>qJ=oafD(J{&!(yCs*wYuU`IXzeQ{)-Ar^_`$@G7_K^nT zWAYcKvo$oU>k;e?j8*3|n7$`PI?Ra@6Gd&GO+@Q!Q#1%iT9#^ZK8}1lsMYD4kP|cB5>A=SD#)?wTfQAIVy`DjiqcHs^bH&IlRf#6S?L+u8 z=6Sek2W_Urgx;(2uL5JGBuLlcePwdC@de-I9QqFqX-MZp$l>)nmphR5(;*{bV*14d zq$X@}A{NE_TriU#9aV4MnYl=~-tK0xU}R@+mf?<2A4+`UEr9>{t=x)>#@K6^EE4rx zhQGt=B3Ld$l*IfBJvBUACfh3%yn6yo@E(A0U;7$W8ag?v3IGv&T+UxCV)pN30>-Y6 zOQ$UBIyw{bHk=P=tP%q^t3TDyX6qeO$l$CHpHDvLpWJ5+VoOKNeB+9Q6Rgl zGq0*b7Cpsu@;~z7@Ghe93Mwtn@s7!VKV(f>%6(Q@X@eolvzwg{2{%ClRWUR$;Vx`Q zb?C#Jp*hx{VCJ2c(Fz8n4};hmn^tKF)XQ zH?((BU#GY6&#%5_8$6LN+Z?5_kB7I!7hACr2@_mNY|;@8biR#2Ypi->)S4q8Mi(O|+x!ZnbV;V+83bbKZ+)}>OB4KY_@0wkc5dN zeli*QMn}G?E1v*QyBvjV%@5`@qpUw0B7WFTi6}}cFU)&uuYr#2!FsP2PP4lpR`t-s z(v8@+a-X*G|hTdbZo!MT?u^-Nk&>nkB%NSrPiU%*PgyaQAwkhRPKPSiyzjpNWwpVrpM zMjNi-jS{+pwGa=lELR7uG02IwWl$V^S+*=Ne%vU47|4Ik|lgAm2pF-Az}M z(-5=vh>Swe=Wo!H_;xxSl2@<1dAu90#$J!^doxVGdk3!K)EW4~@*`?@xcm*R!u%tt zd3e%Oc)ShnnX)M_AKpNZMo<$?Yr(7@RI3}ujklBt9t`q+RH*hY8{)t+c|jc&n=!2= z@CeJrtnpd2Oo0~2#ITlOJ(KYB%0Y*eO&;E-+d46_Aj~nr9iHd~PVmC1jT5f4iy0 z6X1k!e37qORp+58(#%_zUoAAqNk}g8IeT1?IML}Gx|=BeRD#BOq=!ggrp=#g)?*@n z75^>!*v|JMf~!rnt$FZg<`^{W+HcVE=w{OO$k%a4ot9pld(T0i^`o~iSy92VZZu_k zEzQtW4CEqq!F;AiODrc|&IA>|Ns!FE9?cRbH|@vr8lq2g&m=A+xZ>%5Zf0ej_I^=W z=ZoI5=gnfOw1Ft*yj)V1p+@4$Mp_kgwzhJWZ!$Lr@`E5=G#q6eR&7Bnu*X!NjpUsZQ=Do`UF zNL4ziis31|jdm@X*!_u}vLvray1HDf%A$S;>biAOxJ6NhXZ7F@i%vL2VAa?&f<&Wq z+WKH?smVE=85sRt6I6e^odsMD7pD)vN+#$mP`qIb~VnbCHvScFCdpoQU!?LPK1fdRF z-D2DlpO4nUM{=l&rm2x0bYHT&960mqxgKF*@f(U}D|on(h3BQ}cZf4CNr`d+qAB)q zUuC#&siwPdzsWLRXC}T3p+*u?jTlQ^CtM9pyp}uSu3xAWH~&`DE{Dre{h6|l^?)pLBjYO;0)<;U4oqu^(yFxVNKAe%yY3K5)m+^H! zLq=$HXkAcz&emD$f@*2icVBipynZICAhLtzup(nLbl1qnHYmMKN6C=@u1z2jaWNQJ6@-dw`wzB z7*<735c3a#@}vC5Pfm-786HJ%%ou78nI}4BKy~hclHbI>&@Qs|*R(9c4e2vJZxG1P z0e{;Z+nDjJVkaBX7S1Qk1Rdo|BEKCQpNz$YL(k5x3@mi6;yu*X?qrX8>4%M+4)6C? zNdmxy6|)U@y))KGyvUSwH>=$y>)AbL>jYsw!wQCsd}aHXn7J6j+-$tEXSL;(jT)0j zGbDkb5^)^N233Mbgl1pJA}zF&%-L9ss~UoY-qR|VNiZU*Bj-{cNXSaaNP2YzpIFv+ zv+h-VAlifY7e1A-3Ohs;P%M;`#H1#BZZceva8fyDHoBAb?G0X~ha(3ohU-gqQN|_- zK&5n)dyY%=N1azu>Z*#US)fsGv2z6Six@vBUFj+2lePlab_>DmqLjU~+mtgS8wbJ| zPPZ>@5PeCGg<{LhjEHKCI7YhI94RxzEN7V6*}7#Z&_FSYT>w#?5P|#!?%!X>30%0e zTffX(eMUXgFI=-U z>|;9GB4%&Vh!~M`D&epd4W>~Bcyh4pF}ygK7;6!W#BUslT4a5)@E$PUg!0}4W;46j zQZuEi#h{(_{>}5_!M#fBP#||kKKUwR3u0g~wNa11)B4?_1ccma*RvwLIduhzD6oK3 z$_jZv?ZpX;sWh>ohL`;COhbVjqbzWpsZY7%wYFS$uJ_QRu2`K#TS2sBOb4aSd5Vz= zw^Utv>C)2Z7nSpqKd6`m7h~ahzDSVkt3a3(Jcz93TYnUp8Xk!mC`YvK(G;`GfYv9o zJ%qrl8Fe16iOYD#5z4-G&pXA0#rTP?RK^vNOiF?rNr({QwCGZ-mR~TKL#SVpK7u#K zs4*I2ni{%H$!C{Qv@OMEUzt5HQB!CM(KhWO~Tz4ML3b^zwBAe;lia51UqW*i08Fr zPD^?BYs_WmKb8@eKj4aqNww%YgG5fRRb1mW{m5Z93Fy_0#utF}g^@-(q}CR2DD z!VRz5?G3Yo%^Q)cH*%26&2lYvM$cv-qGl*tBoy$MC6Q?u;+-Towp+6fBfHp4(@~7$ zjcQr#vV0;@?-Ez)Tf3d?K7OJIkET>nz$E!q0)Z)(2smxYH5{|jhYbJF?rf8aJ_GucU@pmq+u*fDdD$Hp5vFhnAL0WV+p-f&$G#^|pej@nLyzDN;)$S9 zkswr6@2=E6^nAst1lccSR~;ND-qRuZ6fjQj zq>yQK#0;)qc^z#SqQEH`qdUW+`pq+C?d1l5Fl6{XVJN({)Gix#TyGpF^JklTr8~Gz)Eco4q_uIw zeUW%!;$f5fqsz&RA&v-1HFyhU=fz?`H;?e|s?aZW=)XpJlWM+Ow3=!PC6(5N5;eoq zjC`{e30-w834Mn#dCJdy%cjmXr^7oPvlGL*a>@W~&Q9t6RS4om6D{f-@4rKl1f~I-yC$t%|z5XoM}tg1}U(l#S9y~5Y1LT zSdRb+FZZkmx1FzT$vUA^@*YW5JoP&tH#_)Z`AwLQZv%ECVaPRTsH%-KFWRGulO{3F zBMIWD)?p{Zoc#LtBX$0WD5ly*{hh=8;CFhbrb1aS-5)>6qlg$@C~}NVD&FWR+MIt8 zTeKcNY3cDB1ve+#oWb^nl&u_zkYBch1BYv*%GgWjMZn9(Xr->1is|rn&U>KJ@0e{k zvNJ&>ugKM(3!Dj_(o(@OF==XgL6W}zvn!>ebT}DKD&0N$f@>{UGfqT5MyDsIT1l_YTC*3(E-MB5q*InhsRD9mAs8Lxf{Yf6n7W8xYgkelYpmi(aiihC5YXRXbg;^EO?^b5*`guYAoO+I+3q?ChzyYNgy}9DU=% zE`+xyk-=K>AL#ris|ku7X;UB#UE&S0DbRyV{vpDy6)oQ6B(Hm5=ngKW2shs5=pOL5 zn@ntq*v~wa^S?-(f9}cK-+BN(jIY0xWh}cL2cEB@9q;zhUIZ@7Ib5SEjN+`#udTn< zeYpn_8D$E2%u@cOtS-M;QfBoCxzQc5B*5`nHWEvYbc(j}I4QlqM&VOQ&(UEUd2M0O zcS7@BldLXFkG$zQF#B!l{N3oHlt5%!Y{V>f&#uylUf+?+jn&)AljEC-Ce-+b#~#b5 z3z*%Wnh+YkC>@YpLusK6NbFG=XC7nLsy|#8o@K}6K~v!A^LZL@W_hgg%S?O!244>| z_@e@Mx=8ZiuSLn~>Mp@{E~!UG%w-&)=!GTatx1ZRMB%x}gr8m+bdlb3PaNk` z9vDV{ms?ykdNgPWMny>E4G%p9!bnvH`0%3?oujReDSne(K0ELZMcpV!CtL+`<9B_n zJ;}Y_?44~YZGLAy%Im7yf$ibhT~*pTC?Z!?!p8^9+rUFD2$Dw5!Rx4+q~D z#4MF^TnpNJY%HLem3-X>+sU{;{JCZd#Sg}$s>@5IS#3u5dM`_+6mg!_M5|w4PZy&Y z_(}=@YT@%PFiSY{)#>!$WO!3iQ%V1FgYL?lheFcZ36N-^?!4ya`Afb~VJgwmucXhz z*NqygL9u%t7?$>sw;k^1?PPkABbD7J1+pF}tVX8g5vGzRgGf>@6=>FT;1g@T4g$o# z2P;+yMwFt*-gGYQe5rp#xXS$4L^&r_Xw_kdV`SP@t55!rY^@TZ8r{JSf>-f=C;L$+ zI|^VX(lBXpQ{{0FG~WZ9^Lq=E1t+yXM=r)XM{fkG4ht;t^T--oU$CaeSyugglp~a zlZMsid$Y;0=MV>I_A=>ex07pkv=*v|Fi`0E>d>iUptCYdidvhQz*)J1+}%yX3BkXR z6IuSn9%HqS&*s&_)v&H;@%{t=KHTA zN{eJoKpC3cLsdSR90;Tk=toX2U-v0p*>tBrX$!wJhCdb1@rs+th0`b99k%pm4q+*C zT=G*Or)3C1OWoIk0#{Y;r1x)>W{{d>V`Jy4qF?&?HjuguS!{unmOhB$Z>-3TXQwi} z>fzlWMCwhBNDuSIv(XwU^n8&y3wAH0^3V>P*-UonraYtcb$DCCbb1d^Rn(o_YOdGu zHm=Bts(^V`yyzm&G>vN-#&;;I@gwpzHQZP`KCK{-yBn5Q_z+)2dFp-99>`l**sL3g z=7~F&2_3=gi;X75=@+1IH6pKFDKEWnGJx0!H=dOgSxg>lRptgLHq1w5qLHP;4I**c zOQ?_C;cserXJ*Y;6%@|PW^wTPZ*A%-*@b_G`es6r7yQ(U`b(;Bgu5r_eGa(!*)Z<` zn+<0@tW#MVtZtLBJ!`(aiVx?q+hbT#JK^c_3A2pbW+rBlBs`MLkQ% zHH;`&70&InwhxYFo6gOr>^_xAJwR&D z93);%O%hC9lI@O^up}(>M%RR3pe9nYHhOtWpxcDDlSmBktl4omEo>xM8i)vVu|7*2 ztU7ZpeuploJvVunYWrvr2gwy7kvhBN=RJTxTeJ(K3Rthed}?{@x~S<|pdBxrvbQSD z9pc!bVQoZ&`g7LFX!vyXF!c1vDnx`#L%dK+Z+`1)GUtrw&1BX2RI0IJlPSO;KvpK_ zG|m=C>GRY8W>@%STlfRZ7_&eXq<2QUxm+6g+WL~{3^mf-YV0DiEh;Gq`rDM_NuRtj9A3NZ?IOWXTO@mSbn-77 zPPi&+)r#{o5>8;MKD2IfzIS1ml;ahDdOB5{(}ik4c{+C6)rRT#WVwD*?P=}-sq%v) zGUnX;$JEMd%`-U#TWrfXbGK}>l2sXG3bJIA-4s;GHtoh5;^Jfn2>|6AGviXnB{71A+790U#6KRq{siHwk+lpAU!3GcwQr8{agNvojz_Qi5LGH^rk;j z2m25ELi9+A3ml;q+gtodROO2#w0IR7yc@4~ypMT%Cwe1Ckal>BeCrT%nN9ob!&zU+ z<#{kHM2_N)F0xA3XQ)B+{Nf%sHk$|feT)51?*CY2Q#YeYg+jm~DRJx|Fdmtq9Z{|dc z3p825A_dc2efAGwej@X0lc}3)SfZ2ib{&p^jqZ(_{@*K4ixwbBC%FH243KZpQ zc)fp4$+)o!aoPt6wUNn}3isjyN-8ExXAG=+z+IA*D&q2sABBH#j9I>>R7YcK4u~D2 zHvzrE3l&?J#njb9T9EYvUSZkg6xskw`@-?4n$YJvv!f31$SV1->jatUkY=U1+6-(Z zi`gr5cj{UYrxn`ZqTI1`UBt8Gmn`FeW>FNmbV-;P-;pTXS-rt)B76+aa}WYxDSLUfe93;jAejg9okV4Sul+$dAvJE%T62@_)bKNnX0 zI1w}c^G2i;To+qN;4a))Gq{~0GbJFJsH{xXx@jZaJ=U*oW8_fOXD2*2YZ3mOBbnxj4i-Cqry#Wp&G=&{?nJMj zKReTT(8Z-DCub&C8JWtsUKL@_00J1zVNUC$Xpk*(l9YE&WmRLROsENO_P=h4<wlHp39p`(l{s)7#lIGV_S@yE>mXgnA!c z*}Beb^6)S7TW?uUD*Ve9jmnyo}v^cNed~t z*hOfwbiJz;E!n<;8K*_krcTHE$_`C-yWT^rGV974s=k9WLoGcXPx2?Iltppz#q(cE z3%yRh=p?EcPp0W$u4t zWqCO~=jyl>*Q^_DTB53}F8>yNH^8Dkfc1HxMW5dx+V(O3>Wc98J0A?x#%F~kJlfYG zXq&dx)Q?l9Sp077-1Xirvd-Jkiyf1HOIyaTQa0)6$gP^U&tiqzT$S|&sng}qre<5K zqF5E=>lWXP=UG*V**f%dpB&EZ@Y2Yqs2&6cYWmU+*0ia#8_aFz^RX-F-Q#$(JmIhP z;35nB-JFg>@NaYC42*_}*bR?RkUxP1eQg6mA*pGZEsIdgef%HdE-bSp&ibjC0;%AL z#Sauh$!HACG=PU`HBSkersWJXv1uktpA;(f<5y`7hMCAL5iXC!-GjJywcA!y#+mmG z$4i*$%hfd!r(a1@0EblkFryp0twIr>&G!CXBjT;MzpHZZzWhOBO3{T$B9E2ieq+YV z_6+%Hs@=5JQqks>*yrVN(?{gYcFI#8^Y)gYg z7O3*?KJ^UlNH;AW2K}!u5$iy~N?LM*#Yp`I4Wb5=S%YM7c@cOJ}6~&CP zs9P73WQ$Pjz0rkWe8k?!qgp@qrM||0F+SA8x!=Cic6u*oLc#9h6qv{d;^pswNYQIB z{aw(6MgHc(A$qjP0TZTzC?ec6lN6uPrU(Bpn>=c#UYnWVmX! zkogVL+~Ic2Ur@{1kVd~c;9@Ys{S9Ay!#SE$IE4JP-*t3fA)&BBPX3OB$zu{s?`Dd9 zw~Gusovl^qo>Em=88eL3NK;;*9^#h4APmpVmuWqmO-^2~q|aqRBf-QOa9NsT&=`OS z-_EY}_jfrO40E`&M%5TR2C!4w5(^W48G377T)b!8gq>g-HO_B* z9agV`NFC7J1s_rCvGwZ~ueWUkX)4)%S(&)?d?i~>UKacrT&S$=n#eBoxfW5DPeJE6 zH2{yi6vEOKQPCv4`)(8!T#WEmo9Ibdg`2+1A|HsCf4$gsymUBjOhqtk>O8($5+2UO z|MQO;Ufzsc(uj1Ib&zT;_l3!XL6iNw6)e}HmgIGI zc~zM?o(s`YJV&-3OY5v;28kp(P-$-wd>!PR*NqV*940-$m|I0VK*i!e!NZFWgzW4w z8(%x^Z0D?MbufMH{*r*P-jBKy#;%zTL|Oy=|Kd z%A2MlSa1^&-~99OFZB)8q`2<82R!eA(+#-shvN@8Jn+NAb@3nv9^}A- z9QeoOz<-%Y(ElnCsejNs)ab)_J`^Fh5fBHZe(bR$k<2ZBq_St*uo$o~883Z^d{z>) z`W^ed_3p&z+FaAmPu(IWB0p|Xid3K1d5>l9nCu$udc|w&%w&{tvsmulJuRC!=8bd$Y%`L zo_Qt7L04cHq{c*O`H7^PpVFZRIkHHj;d{l{?#=aeF2rCE)RTo$dzF|f|VLjf;^J;`o|a2UE&Fw*IHoxj*{l9E_;Y?uzTj`>Zk= zkQFegUyLuOIeO1Vp{}TQU6OK8pUT)|uk2^EvupZe7eMl-O|0{`*#@)-1(4drKNbhA zCRyoa2hKCM)9xu{-S6i@Pv2LdQtuKLF@D>XZ8=P4g{!2o-p&A0=Jt73g$F%LSzt)A zz6Y2Hx69WC&EXA0zvOPK5u5cmxk1R9GY`m`jt%RTFins&2Ug;Y`r9Yio%0`yF<0n8Om7abVfoPhvpXK z4F_o_CT!QLptD(>+Ghrw#b^9;t?R-lqOXB+3Dt@T?(0jF(9WxwHEDTTX;XCuuV#>= zu*E+J@V_92?C$~Wrd8?Md!TsOXJaBil_BItz=yZqBJ0Pyjx(>DfPZOoYv>ErSk*heO_G47EwClqaT6ejx+^NlThk5fzKyrM2^@Ik;yU?jB%hBlG zNhbBGUr9&j)*(i4+Ab_n!|U`SwYlq~C)Y_d<+Y_%iIeP)UeGXhr#rSr`Gr~%?ek9V z-V_(&w9G%JhXt3a^edo%%QWQjUr*I}ZaWCp2Foj#wl3MuTJzKYB$o&* zi8^JzB4Dom*2~ywa!zkBskWZ$B=gXHSezJ z?t2Qe&j)Xsp0DJ#bV|Ln%pR*i6=q!=g7BvAse=gwV>?<`Zhw#kxo(F|Tv7&^g{+0p zm}`4l^*)wtAtDb(2)lZ!tmwDZ*}cq=`jW^$&k#?Bp4``or_IOsBCvsf=6&3(E>G9l z=L%kh^?hV2;dxW8Z&ueS{O84cp@OrZ2woS4nX7rQdOT)+XNeWg$gFe_Rt7JBD^^Px zVPs>Xb281S4TP14i6oCIUfO@23$LKp(5c5!KRtHv=jx(oLn_m^jL7Zddw~6}iS393 zL}o*q6UaiDII&CgCauP)CCn{M?50#hT@dCZJws1IfF;v<8M>`&yNdT>^|4N=m_VkP zi^85pNKvZWNst?iE=(DPG4W(4>1-ZZL>J9nB^4B;2ywkaa6P8}O)^K|Nqs2!I)bXD zhjvYc?2mUdcVav>)>26}A;w|cuX;*x&A#xXrq&V-xO`HqaDh2kJ_@Tg6*sNf5%)(DLVhM-z&Rdt7?WdiFS9>hJ_tZ(Im^z7Ej7v0nfR%$O<)$ zYr2RkII5~cwL<*m73B}&;kDYbuJ~X8<^^A%7HP5DmrL}) zzOd(V@UTtXOHBaxic!^s_%uAaDFy~5;fgath2h6TC$~tT(#1tIng$8*o!cXRtHg1& zC5V`GLn>vFA3TKRUG>RgIbm;+!>6j#0=0F+UM>R!ZSBo;{wQX%rmFJesMAcQDwao* zSxzsVtZ0;%l;cw_uXiFcsZKGbbpMEM;3@OW6V+Lo8R6S1T=Lzihnl0krsW;?SVZ{O z?`d{lYg6~Yi}**}R@pBR_)QT~v-FKhcz(!Y5{<5Vdx!RTyYO!<(>`hV&^IwxnKSF+ zsE+Kk(idX-JznAaFl4bjYDL@QI=p-q_qyF6sM#GYAo2Shmf`>% z>)Reg_r)WCMju0705=sUzY_M^F}6$0ZO7tK51*7KUZ)CmCW=rS?t*ansrC`=gu)^b zC~1{}{{X&p=Oj%K8pl+lxLwgjfx@V0)qWB8m~kjw;vhAB8mZK6nR6Vl8V1*rcU7@J z)~9x9HaMiYyRsV0PO*rRxLLsAFUqGtTDn@h(^AP&KRiUJ%g`sj-W| z3SLSMYyCZdO09yl*4+6GnjrP@lEc~)@$$mTjAqqwIWXAST7E|F@LTqK^>fwJ(wPwn zynd%YChqp1iP8O!+A3qmQsO)&ld=G`nBouSLbbP2smk2;Sh3`ADq{Mzr1~F6YgfM= z1Drcp1Hr~13E`>WU8Bm;j;0uDF@OGITJ(>-$mkW;h}Y{tGAZ2f=I>eBzcRJ|PRab+ z_^)}Nt`myWL4~zXpMuP`1ou+1`#lJ#vQyme%*JwMEq#_ypv(9azIoyTDoxE zMuZYA?)cR~ukQ&RADpj9O^1D*_K-dIF&=b!cH4ed!Nr9*^Yt6E$B*I>&@eb6-p}>$ zJkph;OLKFx%S9Jg967D5s;qqyx)V0S^vc;6L;M>xnOhh9qY3EMMw6mD(>v4~C@HGCmP5LHZQ#v83FWEDTEL+}(RQcN> zc~RDdA=39c+3osWNe@eB+2`3DkZ9%p?&1MTI1D^HKfmNh<8T|e>Tq7LJaO1N5+i#* zYsz92K~QRAN6_@FWNxGqRhrGuN=6#)yAVblp&LdJy(i%JEwR87sZlm+c+ z*S_0n0g*d^#>iUmMO^p^m3*avvjJEtCbFf3(Z#B;!n>qEL=1 z7ftIoi(0M3Wevm*%@Hn!rAiWfO=p$O@##vtt%$h?#4RdS9rrI_56@N6J{u6Vy!VWa zv8UfNt%- zNOzyncuaAu$To_YmW}WTU049Y{!s*l0^P+z+I-ZN&kog@SoBc}Q-V-cFg+;g8KfHAA3dAL*hNFj0O%lZ#kO zBrJF=MwWs%pjn)eDyTBPT9pu#*g;zzl8NB<;iF2qBj;lsW@bBc!xHl2>11X>Y;)iLe5zT{HKEte|XW|HEwQpZ>ij(mzzbwP)>ohFt~q@;=1C)<&_o z`1q-|2fFC)6#Lw5`P;Q7s}>HP|{gKjS(Uvvpu46HTf7LA`x+Ckj{RAd=uR1F_h zz@3wlOVdLsjpie_VJZGFqlf5`paoOp_e1$?^tqpn$%by+2YHlotZPb6tj!&&F;vgG z;q8ou($|YaZn~9v%d8chzqOEeM#RI7{{tpaypUyG&aFKr?OU4X0uz_ zvss}lKW$hf$G;R^U1YgSFFy|ZcMD+fS+$4v{>(E?j+5@3AJdwf!pJ3SBsEhJJ>Q{aMy_9-2nl6<;`8qDqC+x>4(! z(6Hi7M2dMlF|E&-owE@1F318YS>W4LTxJC(;$I(@0%>+FEk(Ud8k3)JWV7!9GKy6b zL5?KIsf^c@<)Ik`X3(h@-SQp_BRcufpj)czz=2aV&N{8Ps!+VJAaSH>fU+JL7+%)n zRo#a-pUR}ezV#fZGj+a>u*c+Y-qy{Qgrr+$*P0t~<$uzAjou>D1DtljM_Rz5S{)o@ z^rS8|dNs^zYC2984&SV09#qcf(d9WUgVVVdW^Ti&%V(8Q$`lZNX+s1y1XZ}qQyIp& zBX{H^_vP@JGl+_p7S{U+?*aYr{C2db(E=cKJ&FI(-kFC(-MtNbEZHeSBwHmkh%oj& zgk)bDrpP`XgREnxNE&;YkR|&zWM>$XkQACRWE%{!Pi0@C-@L!~x_M5e;ok6mVs% z>wpW~hanqWDh&U35X~QLbrH#B>hFU0w#*AXL@ zN05@;nQZh9k;W9S4w6e}SHR}~@`7@LXJGf}{!w%9cWg>l5I0N^>2S47F)PkGN`E$T zQ}*Pqu&AW|0OPL{%NwYt4LPOOw(nd*I)GlvGx0 z6r_gVtD{GM;-^iQewUZnjHzkGdy+0n7rvfsMi}tq10@De2>?oFVJ8*(U2{4?jgnJ^ zU0r?kyAN*c8S5o-U0{s3sW7_~hyPiV45)%vV}&2~;L78fPW9f&b(fI>35wqrHY;Xk zL)nJ=zwHs^ior)fXEqX1!yFM4i&YJp(*zo;7U^Mn$foMnVRjp){o1|k| zKl4~TSo{8sA*W{78Ap4O)E$Jm-F$Wb^mR|;?yT~_5NJkp)++ev{DW=i%4r+A*v@Ib zi@Myex^-CSw@_-o0^#_6J$8VE?lBt!6ZW0jDfx@(m7J12<8u~wu<7t(t#BFUdyv`4 z##o?KkGUjY;zGt5@-@4W>40ht9}krD%~=h2vbp)h35aPKBHp5 zck;<&Tu*HiRBrK~u(vlHzk>ne$hc!MUnYCdoBgysK6u`BuQwjrrZ>+d%*`#Rl+CkK z#H+=KU&{FGnEjdFxyhg)Fx`i`tR>)*#<>JK1zPMgWuWWFJ19JU@;#C$ErwNm$du!a zghmv21#(S$k0?Dcrm;wP(2((B$ho~0gO=oCoM7+%&{18_p+4tF)viE9^%i$ZlrtZs zZP-W8SiBvxc$b=-&@ONYCC7zKwx8L=rL#>LV?f?%>#OO5-4P2B{$B)KBCI2Oa!V>& zfu1hj4#MMAE)ccz!=ez-wL<;1J#$Vj73);8Pr*c9gp}IMnFt#h)~VY%oQsn-H8swo zeNswSTkki=@CVSOjdyZ3d}*DSG!(>~saQv+tW%E;@X{?%ajivmd6?)-jck3Ay9$#z zk#ZE=VyL;@QK@t#iKP8eW%PIqH zfZWMLav+Gd@cm8uAAr{O9WrBjJL|akSSH64;&?S2tAbISlCFliljqyh7 zfe?N_GkKVcwa9P3j8b>jbm{h`k6X=W#X(03&PEtU%9N9#2r7pub{NmuC;x>@{%<~V zT>tNY9>>6L9%(7NQwQhl9-MVk$1*EyQ?leeLT#)Yup>KYB}=5`g_g6ia$>X zx^SK5b?778u=DX_8iS+Q%}id-UJu`K-zdS}F$JH)&nkL5vUh9;#bx`vglKr0+T7in zc2j6p&3^!-28RnldTb(NdR*rP#NPJ+D26ilpIx~i{CdA&zbNkP?q$Nb{`QDQ5IolF z`nEEEcLOl<>vd^2Iq z=dcGpVUr}WZ(gV_4KXPFkRRy$17K2bO>lR;5nDrPp1cwTlp`|1j+mQLWL!=eFp=0D z*Yr4^9RW^Tgg8~XAqn*8sMq_1f?+2TfPC=uK6j zbGCi}SbVnwOvV-^ZK34s93s^;!RwatbL5b3_v@%uz0Jm;oB`XPiQOc|XBq;%OpzlO zssxCwV{`9))hNsLTsU4d_PTFg@%+4}<@}Xy;fq6deJNj5Z6$gvroLcYE7o@?55)3#LCKmvm&%flusKCQZF*;F~m|Am|XHw3yv zvH4C3ZCo~KMr`$S^AonDZmgt9iaNJ^rB$a~j>|}Yyq|lKo)2$MI`4J{fccbN3nZwt zpXI8?8U#YNGL0L^cAkinkyzF7%5^+?+6T@kRuWG@bMRiDB>5;>z93@mj^RwBeO<+1 z`qu@+W#?J>!gd8){0&7;qYgXnbRkZ%pvFv3wv8NHM9g`nFlw zjJuC0>?_Pds|+8K-PyfyrLrPDnKtDZ96H$l(<00dbGd&NJh|-eWqa-}H;mPGp_uw} zg;B6Obg@xR@->!5!alkOxL?URxtj^cJ*gsSHfc zlMR&qSmCF{Y{*K@9U*6+Z&$otu=f0)!n4F z-OBFf%Q$1`q(wj4;@n>9;u<*t9txxRr~iw~1YXr#JIl>&{#0FEEc9w%u|@C!vIbWv z;Sgnji~;$@2&N@PCglcY7}=xVffVa!mlq82Vs{&|ayN*s5=?KGdE6=WMXA{^En6t) zg0hN?83BWz8h64ABk1`&R0Hom(p3eYQ3p`(&nF7MsKC*v;vE} z%68$FvzF%}*YaV*uuqZO@I>mq^=w7NIY-}1Xo!+}EtbTzjf1kx`zQO9+S*E}O(uVG zNoe{&XB3WH{-D^SDyTtB&fjBxSvgfl+PgL$uxH%^$pQI*vGQ@kNilY1*3qVVJ-NJw zUQ?s0f}Sj(JfX%jhTexo;>@Xg2OVz{1JV(zJgN&fRTh_3AKv=!)7s5@6Z!5d@WR`+ zs^w=fFAa3P&vR+UewwAzqIr8)b=z&sD^(9u*ECcCD~N-fXAFFASta^tMCfBwl$8=P zHFAAD@CCUi{3CO|V9O$OT1sPo)Cr-E6YK0ij{d+S5;P`}m}v-RTxnN)2jscM0Dt|> zdYVm6zWdZU$B-U@g}8wGsJXhYg$1XxCV@_Gv6SE<=FbZS}|TBX*HyXxc`i1WRS^iKoG%kzxFqQ<_}g^FIUv@qc) zam9VOto(ztHjDD4=~pH9yrb>R=WS?wO&YTlMdgp6e&QuQWc02wV)pm(43Hxv^(N7j#1$``n=)#jG0i zsJ%+ZpS*h$iDvQ;Q?=cV}~Tr`on?=f-q@w4A2eKv1lB zAcCdrD4ZIFTDrfMD8yVs_}`JDnA`$p=nmGSd{}q0x5tzO?hPy6dwB~(^>!k874X5A zoLIwzO=MVp=)4F+Sw4W`lUd2Xdswqzpz#e43q~P4nPk@kg3t3l4m8kbBuUz;q|DUa z*&r@B4@1gvD%Ctc^viS$0yLfarZRVbI-xGFdyqS}{sB)kz0`Wo7j`B9A(skqHakus z3+26DPI$xU1VzF6Jm}n=L@8JkCq=mg!_ks}S?n`kj4}Z;Q{5nUvxP5QD4=GROB|7$ zfQQB@6i(PyuUdsFzB8b35Xx!+pI{Il6OxKhWNzxdEF$9}F+6Nz;{n3*z}ttl&M}ny z02tFSL-G>f;1QPVUmO&CV}B8wf86=?mRrhOjfu2~Vq%$ko?>IqG(?f^6#f-FP-Xi*fU3l~F4E6B7OZ{Pv=U(Y55y0*JFu zNvXBAXYt()hleWy{Ei=wrExqNj@QDUsS19NcRPOf*WwS4$K!ZBeme*Fe~kVO7=)TM literal 0 HcmV?d00001 diff --git a/images/img2.jpg b/images/img2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af3a5897adcb45c2deda6e7f491214989ccfe7ae GIT binary patch literal 59799 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 s-zax9G)5Bx-SUFw6}oTUK%J Date: Sun, 26 Dec 2021 20:41:07 +0100 Subject: [PATCH 03/15] improved README.md --- README.md | 10 +++++----- src/Clockify/Reports/ReportUtil.cs | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 91b8d53..261aa3b 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,10 @@ Important: if you test the bot locally, you should use a reduced set of settings ``` You still need the LUIS service to be active. -###LUIS +### LUIS For proper operation, you must provide a LUIS model. This can be done at luis.ai -####Intents +#### Intents You must create different intents. The intents below are the ones that i've figured out to be the minimum. Add also @datetimeV2 as a feature. @@ -107,19 +107,19 @@ Add also @datetimeV2 as a feature. ![images/img2.jpg](images/img2.jpg) ![images/img3.jpg](images/img3.jpg) -####Entities +#### Entities You need also at least one additional entity called "WorkedEntity". This stores the project you have worked on. ![images/img4.jpg](images/img4.jpg) -###Auto reminder +### Auto reminder The auto reminder is triggered by an endpoint. You have to call [GET] http://localhost:3978/api/timesheet/remind and pass ProactiveBotApiKey within the header and pass as value the "ProactiveBotApiKey" value. ### Clockify The first time you contact the bot, he will ask you for your clockify API-Key and stores it within the KeyVault. -###Run +### Run Then run the bot. For example, from a terminal: diff --git a/src/Clockify/Reports/ReportUtil.cs b/src/Clockify/Reports/ReportUtil.cs index 3fb65c9..6f486c1 100644 --- a/src/Clockify/Reports/ReportUtil.cs +++ b/src/Clockify/Reports/ReportUtil.cs @@ -3,7 +3,6 @@ using System.Text; using Bot.Clockify.Models; using Microsoft.Bot.Connector; -using Microsoft.Recognizers.Text; namespace Bot.Clockify.Reports { @@ -51,7 +50,7 @@ public static string SummaryForReportEntries(string channel, IEnumerable Date: Sun, 26 Dec 2021 20:45:29 +0100 Subject: [PATCH 04/15] added hours to the summary --- src/Clockify/Reports/ReportUtil.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Clockify/Reports/ReportUtil.cs b/src/Clockify/Reports/ReportUtil.cs index 6f486c1..3fb65c9 100644 --- a/src/Clockify/Reports/ReportUtil.cs +++ b/src/Clockify/Reports/ReportUtil.cs @@ -3,6 +3,7 @@ using System.Text; using Bot.Clockify.Models; using Microsoft.Bot.Connector; +using Microsoft.Recognizers.Text; namespace Bot.Clockify.Reports { @@ -50,7 +51,7 @@ public static string SummaryForReportEntries(string channel, IEnumerable Date: Mon, 27 Dec 2021 00:30:25 +0100 Subject: [PATCH 05/15] added PastDayNotComplete.cs --- src/Clockify/PastDayNotComplete.cs | 68 ++++++++++++++++++++++++++++++ src/Clockify/Reports/ReportUtil.cs | 2 +- src/Startup.cs | 1 + 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/Clockify/PastDayNotComplete.cs diff --git a/src/Clockify/PastDayNotComplete.cs b/src/Clockify/PastDayNotComplete.cs new file mode 100644 index 0000000..a301fa4 --- /dev/null +++ b/src/Clockify/PastDayNotComplete.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bot.Clockify.Client; +using Bot.Common; +using Bot.Data; +using Bot.Remind; +using Bot.States; + +namespace Bot.Clockify +{ + public class PastDayNotComplete : INeedRemindService + { + private readonly IClockifyService _clockifyService; + private readonly ITokenRepository _tokenRepository; + private readonly IDateTimeProvider _dateTimeProvider; + + public PastDayNotComplete(IClockifyService clockifyService, ITokenRepository tokenRepository, + IDateTimeProvider dateTimeProvider) + { + _clockifyService = clockifyService; + _tokenRepository = tokenRepository; + _dateTimeProvider = dateTimeProvider; + } + + public async Task ReminderIsNeeded(UserProfile userProfile) + { + try + { + var tokenData = await _tokenRepository.ReadAsync(userProfile.ClockifyTokenId!); + string clockifyToken = tokenData.Value; + string userId = userProfile.UserId ?? throw new ArgumentNullException(nameof(userProfile.UserId)); + var workspaces = await _clockifyService.GetWorkspacesAsync(clockifyToken); + + TimeZoneInfo userTimeZone = userProfile.TimeZone; + var userNow = TimeZoneInfo.ConvertTime(_dateTimeProvider.DateTimeUtcNow(), userTimeZone); + + var userStartDay = userNow.Date.AddDays(-1); //Get past day + + //Check for weekends. If we got one, go back in time. + while (userStartDay.DayOfWeek == DayOfWeek.Sunday || userStartDay.DayOfWeek == DayOfWeek.Saturday) + { + userStartDay = userStartDay.AddDays(-1); //Go back in time till we have no weekend anymore + } + var userEndDay = userStartDay.AddDays(1); //Add one day to the startDay for a 1 day range + + double totalHoursInserted = (await Task.WhenAll(workspaces.Select(ws => + _clockifyService.GetHydratedTimeEntriesAsync(clockifyToken, ws.Id, userId, userStartDay, + userEndDay)))) + .SelectMany(p => p) + .Sum(e => + { + if (e.TimeInterval.End != null && e.TimeInterval.Start != null) + { + return (e.TimeInterval.End.Value - e.TimeInterval.Start.Value).TotalHours; + } + + return 0; + }); + return totalHoursInserted < 6; + } + catch (Exception) + { + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Clockify/Reports/ReportUtil.cs b/src/Clockify/Reports/ReportUtil.cs index 3fb65c9..3f92bd4 100644 --- a/src/Clockify/Reports/ReportUtil.cs +++ b/src/Clockify/Reports/ReportUtil.cs @@ -51,7 +51,7 @@ public static string SummaryForReportEntries(string channel, IEnumerable(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); From d7d8cfbb2fb9b65f53a83ab8fbfae1e2c7ab78e1 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 10:40:00 +0100 Subject: [PATCH 06/15] added specific remind functionality --- src/Clockify/ClockifyController.cs | 11 +- src/Clockify/ClockifyMessageSource.cs | 2 + src/Clockify/EntryFillRemindService.cs | 20 +-- src/Clockify/IClockifyMessageSource.cs | 2 + .../Clockify.ClockifyMessageSource.resx | 3 + src/Remind/CompositeNeedReminderService.cs | 45 +++++- src/Remind/GenericRemindService.cs | 5 +- src/Remind/ISpecificRemindService.cs | 10 ++ src/Remind/SpecificRemindService.cs | 143 ++++++++++++++++++ src/Remind/SpecificRemindServiceResolver.cs | 25 +++ src/Startup.cs | 3 +- 11 files changed, 238 insertions(+), 31 deletions(-) create mode 100644 src/Remind/ISpecificRemindService.cs create mode 100644 src/Remind/SpecificRemindService.cs create mode 100644 src/Remind/SpecificRemindServiceResolver.cs diff --git a/src/Clockify/ClockifyController.cs b/src/Clockify/ClockifyController.cs index 6194fe5..08fa7b1 100644 --- a/src/Clockify/ClockifyController.cs +++ b/src/Clockify/ClockifyController.cs @@ -11,18 +11,18 @@ namespace Bot.Clockify public class ClockifyController : ControllerBase { private readonly IProactiveBotApiKeyValidator _proactiveBotApiKeyValidator; - private readonly IRemindService _entryFillRemindService; + private readonly ISpecificRemindService _entryFillRemindService; private readonly IBotFrameworkHttpAdapter _adapter; private readonly IFollowUpService _followUpService; public ClockifyController(IBotFrameworkHttpAdapter adapter, - IProactiveBotApiKeyValidator proactiveBotApiKeyValidator, IRemindServiceResolver remindServiceResolver, + IProactiveBotApiKeyValidator proactiveBotApiKeyValidator, ISpecificRemindServiceResolver specificRemindServiceResolver, IFollowUpService followUpService) { _adapter = adapter; _proactiveBotApiKeyValidator = proactiveBotApiKeyValidator; _followUpService = followUpService; - _entryFillRemindService = remindServiceResolver.Resolve("EntryFill"); + _entryFillRemindService = specificRemindServiceResolver.Resolve("EntryFill"); } [Route("api/timesheet/remind")] @@ -32,7 +32,8 @@ public async Task GetTimesheetRemindAsync() string apiToken = ProactiveApiKeyUtil.Extract(Request); _proactiveBotApiKeyValidator.Validate(apiToken); - return await _entryFillRemindService.SendReminderAsync(_adapter); + return await _entryFillRemindService.SendReminderAsync(_adapter, + SpecificRemindService.ReminderType.TodayReminder | SpecificRemindService.ReminderType.YesterdayReminder); } [Route("api/follow-up")] @@ -42,7 +43,7 @@ public async Task SendFollowUpAsync() string apiToken = ProactiveApiKeyUtil.Extract(Request); _proactiveBotApiKeyValidator.Validate(apiToken); - var followedUsers = await _followUpService.SendFollowUpAsync((BotAdapter)_adapter); + var followedUsers = await _followUpService.SendFollowUpAsync((BotAdapter)_adapter); return $"Sent follow up to {followedUsers.Count} users"; } diff --git a/src/Clockify/ClockifyMessageSource.cs b/src/Clockify/ClockifyMessageSource.cs index 2938c9a..007d397 100644 --- a/src/Clockify/ClockifyMessageSource.cs +++ b/src/Clockify/ClockifyMessageSource.cs @@ -46,6 +46,8 @@ public ClockifyMessageSource(IStringLocalizer localizer) public string RemindEntryFill => GetString(nameof(RemindEntryFill)); + public string RemindEntryFillYesterday => GetString(nameof(RemindEntryFillYesterday)); + private string GetString(string name) { if (!_localizer[name].ResourceNotFound) return _localizer[name].Value; diff --git a/src/Clockify/EntryFillRemindService.cs b/src/Clockify/EntryFillRemindService.cs index a673169..99201e8 100644 --- a/src/Clockify/EntryFillRemindService.cs +++ b/src/Clockify/EntryFillRemindService.cs @@ -8,30 +8,14 @@ namespace Bot.Clockify { - public class EntryFillRemindService : GenericRemindService + public class EntryFillRemindService : SpecificRemindService { - private static BotCallbackHandler BotCallbackMaker(Func getResource) - { - return async (turn, token) => - { - string text = getResource(); - if (Uri.IsWellFormedUriString(text, UriKind.RelativeOrAbsolute)) - { - // TODO: support other content types - await turn.SendActivityAsync(MessageFactory.Attachment(new Attachment("image/png", text)), token); - } - else - { - await turn.SendActivityAsync(MessageFactory.Text(text), token); - } - }; - } public EntryFillRemindService(IUserProfilesProvider userProfilesProvider, IConfiguration configuration, ICompositeNeedReminderService compositeNeedRemindService, IClockifyMessageSource messageSource, ILogger logger) : base(userProfilesProvider, configuration, compositeNeedRemindService, - BotCallbackMaker(() => messageSource.RemindEntryFill), logger) + messageSource, logger) { } } diff --git a/src/Clockify/IClockifyMessageSource.cs b/src/Clockify/IClockifyMessageSource.cs index a99f508..398a119 100644 --- a/src/Clockify/IClockifyMessageSource.cs +++ b/src/Clockify/IClockifyMessageSource.cs @@ -29,6 +29,8 @@ public interface IClockifyMessageSource string RemindStoppedAlready { get; } string RemindStopAnswer { get; } string RemindEntryFill { get; } + + string RemindEntryFillYesterday { get; } string FollowUp { get; } } diff --git a/src/Common/Resources/Clockify.ClockifyMessageSource.resx b/src/Common/Resources/Clockify.ClockifyMessageSource.resx index 3660ce1..60f3d31 100644 --- a/src/Common/Resources/Clockify.ClockifyMessageSource.resx +++ b/src/Common/Resources/Clockify.ClockifyMessageSource.resx @@ -157,4 +157,7 @@ Please don't exceed one year window Hey 👋 I noticed you never setup a Clockify token...{0}{0}Help me help you! Once you're setup I will assist you in your daily time tracking. + + You have not filled in all of your hours for yesterday. Please do so! + \ No newline at end of file diff --git a/src/Remind/CompositeNeedReminderService.cs b/src/Remind/CompositeNeedReminderService.cs index c004ed4..2216469 100644 --- a/src/Remind/CompositeNeedReminderService.cs +++ b/src/Remind/CompositeNeedReminderService.cs @@ -1,13 +1,15 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bot.Clockify; using Bot.States; namespace Bot.Remind { public interface ICompositeNeedReminderService { - Task ReminderIsNeeded(UserProfile profile); + Task ReminderIsNeeded(UserProfile profile); } public class CompositeNeedReminderService: ICompositeNeedReminderService @@ -19,10 +21,43 @@ public CompositeNeedReminderService(IEnumerable services) _services = services; } - public async Task ReminderIsNeeded(UserProfile profile) + public async Task ReminderIsNeeded(UserProfile profile) { - bool[] conditions = await Task.WhenAll(_services.Select(service => service.ReminderIsNeeded(profile))); - return conditions.All(c => c); + var reminder = SpecificRemindService.ReminderType.NoReminder; + + //Check every reminder within all services + foreach (var service in _services) + { + var serviceType = typeof(PastDayNotComplete); + var reminderIsNeeded = await service.ReminderIsNeeded(profile); + + //Check if the particular reminder was set to true + if (reminderIsNeeded) + { + if (service.GetType() == typeof(PastDayNotComplete) || + service.GetType() == typeof(TimeSheetNotFullEnough)) + { + if (service.GetType() == typeof(PastDayNotComplete)) reminder |= SpecificRemindService.ReminderType.YesterdayReminder; + if (service.GetType() == typeof(TimeSheetNotFullEnough)) reminder |= SpecificRemindService.ReminderType.TodayReminder; + } + } + else + { + //As soon as one reminder check was negative, we return "NoReminder" since all checks needs to be true! + //TODO not always break! We can remind for yesterdays times even if it is early morning! + return SpecificRemindService.ReminderType.NoReminder; + } + } + + return reminder; + + // foreach (var service in _services.Select(service => service.ReminderIsNeeded(profile))) + // { + // + // } + // + // bool[] conditions = await Task.WhenAll(_services.Select(service => service.ReminderIsNeeded(profile))); + // return conditions.All(c => c); } } } \ No newline at end of file diff --git a/src/Remind/GenericRemindService.cs b/src/Remind/GenericRemindService.cs index c645336..e2a0795 100644 --- a/src/Remind/GenericRemindService.cs +++ b/src/Remind/GenericRemindService.cs @@ -37,13 +37,14 @@ public async Task SendReminderAsync(IBotFrameworkHttpAdapter adapter) { var reminderCounter = 0; - async Task ReminderNeeded(UserProfile u) => await _compositeNeedRemindService.ReminderIsNeeded(u); + async Task ReminderNeeded(UserProfile u) => await _compositeNeedRemindService.ReminderIsNeeded(u); List userProfiles = await _userProfilesProvider.GetUserProfilesAsync(); + //Fetch all users where the ReminderType is not set to "NoReminder" List userToRemind = userProfiles .Where(u => u.ClockifyTokenId != null && u.ConversationReference != null) - .Where(u => ReminderNeeded(u).Result) + .Where(u => ReminderNeeded(u).Result != SpecificRemindService.ReminderType.NoReminder) .ToList(); foreach (var userProfile in userToRemind) diff --git a/src/Remind/ISpecificRemindService.cs b/src/Remind/ISpecificRemindService.cs new file mode 100644 index 0000000..aa2424d --- /dev/null +++ b/src/Remind/ISpecificRemindService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Integration.AspNet.Core; + +namespace Bot.Remind +{ + public interface ISpecificRemindService + { + Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, SpecificRemindService.ReminderType reminderTypes); + } +} \ No newline at end of file diff --git a/src/Remind/SpecificRemindService.cs b/src/Remind/SpecificRemindService.cs new file mode 100644 index 0000000..a7bd00e --- /dev/null +++ b/src/Remind/SpecificRemindService.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bot.Clockify; +using Bot.States; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Bot.Remind +{ + public abstract class SpecificRemindService : ISpecificRemindService + { + private readonly IUserProfilesProvider _userProfilesProvider; + private readonly IClockifyMessageSource _messageSource; + private readonly ICompositeNeedReminderService _compositeNeedRemindService; + private readonly string _appId; + private readonly ILogger _logger; + + private static BotCallbackHandler BotCallbackMaker(Func getResource) + { + return async (turn, token) => + { + string text = getResource(); + if (Uri.IsWellFormedUriString(text, UriKind.RelativeOrAbsolute)) + { + // TODO: support other content types + await turn.SendActivityAsync(MessageFactory.Attachment(new Attachment("image/png", text)), token); + } + else + { + await turn.SendActivityAsync(MessageFactory.Text(text), token); + } + }; + } + + protected SpecificRemindService(IUserProfilesProvider userProfilesProvider, IConfiguration configuration, + ICompositeNeedReminderService compositeNeedReminderService, IClockifyMessageSource messageSource, + ILogger logger) + { + _userProfilesProvider = userProfilesProvider; + _compositeNeedRemindService = compositeNeedReminderService; + _messageSource = messageSource; + _logger = logger; + _appId = configuration["MicrosoftAppId"]; + if (string.IsNullOrEmpty(_appId)) + { + _appId = Guid.NewGuid().ToString(); + } + } + + [Flags] + public enum ReminderType + { + NoReminder = 0, + TodayReminder = 1, + YesterdayReminder = 2, + WeekReminder = 4, + DicReminder = 8 + }; + + + private bool SendSpecificReminderType(IBotFrameworkHttpAdapter adapter, UserProfile userProfile, + ReminderType reminderType) + { + var callback = BotCallbackMaker(() => _messageSource.RemindEntryFill); + switch (reminderType) + { + case ReminderType.TodayReminder: + callback = BotCallbackMaker(() => _messageSource.RemindEntryFill); + break; + + case ReminderType.YesterdayReminder: + callback = BotCallbackMaker(() => _messageSource.RemindEntryFillYesterday); + break; + } + + try + { + //TODO Change _botCallback according to the reminder type + ((BotAdapter)adapter).ContinueConversationAsync( + _appId, + userProfile!.ConversationReference, + callback, + default).Wait(1000); + } + catch (Exception e) + { + // Just logging the exception is sufficient, we do not want to stop other reminders. + _logger.LogError(e, "Reminder not sent for user {UserId}", userProfile.UserId); + return false; + } + + return true; + } + + public async Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, ReminderType reminderTypes) + { + var reminderCounter = 0; + //Check, whether we need to remind at least one event + if (reminderTypes != ReminderType.NoReminder) + { + async Task ReminderNeeded(UserProfile u) => + await _compositeNeedRemindService.ReminderIsNeeded(u); + + List userProfiles = await _userProfilesProvider.GetUserProfilesAsync(); + + //Search for all users where a reminder was set to something else than "NoReminder" + List validUsers = userProfiles + .Where(u => u.ClockifyTokenId != null && u.ConversationReference != null) + .ToList(); + + foreach (var userProfile in validUsers) + { + var userReminderTypes = ReminderNeeded(userProfile).Result; + + //Check if we need to remind the user + if (userReminderTypes != ReminderType.NoReminder) + { + //Switch between the different reminder types + switch (userReminderTypes) + { + case ReminderType.TodayReminder: + if (SendSpecificReminderType(adapter, userProfile, ReminderType.TodayReminder)) + reminderCounter++; + break; + + case ReminderType.YesterdayReminder: + if (SendSpecificReminderType(adapter, userProfile, ReminderType.YesterdayReminder)) + reminderCounter++; + break; + } + } + } + } + return $"Sent reminder to {reminderCounter} users"; + } + + } +} \ No newline at end of file diff --git a/src/Remind/SpecificRemindServiceResolver.cs b/src/Remind/SpecificRemindServiceResolver.cs new file mode 100644 index 0000000..d788967 --- /dev/null +++ b/src/Remind/SpecificRemindServiceResolver.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Bot.Remind +{ + public interface ISpecificRemindServiceResolver + { + ISpecificRemindService Resolve(string name); + } + + public class SpecificRemindServiceResolver: ISpecificRemindServiceResolver + { + private readonly IEnumerable _remindServices; + + public SpecificRemindServiceResolver(IEnumerable remindServices) + { + _remindServices = remindServices; + } + + public ISpecificRemindService Resolve(string name) + { + return _remindServices.Single(p => p.GetType().ToString().Contains(name)); + } + } +} \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index dab8999..80b53ed 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -83,7 +83,8 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); From c314fc8100d7c0643400af38ef9f69c009de9636 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 10:46:31 +0100 Subject: [PATCH 07/15] Check for every service and set flags appropriate --- src/Remind/CompositeNeedReminderService.cs | 26 +++++++--------------- src/Remind/SpecificRemindService.cs | 4 +++- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/Remind/CompositeNeedReminderService.cs b/src/Remind/CompositeNeedReminderService.cs index 2216469..e9442b0 100644 --- a/src/Remind/CompositeNeedReminderService.cs +++ b/src/Remind/CompositeNeedReminderService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Bot.Clockify; +using Bot.DIC; using Bot.States; namespace Bot.Remind @@ -34,30 +35,19 @@ public CompositeNeedReminderService(IEnumerable services) //Check if the particular reminder was set to true if (reminderIsNeeded) { - if (service.GetType() == typeof(PastDayNotComplete) || - service.GetType() == typeof(TimeSheetNotFullEnough)) - { - if (service.GetType() == typeof(PastDayNotComplete)) reminder |= SpecificRemindService.ReminderType.YesterdayReminder; - if (service.GetType() == typeof(TimeSheetNotFullEnough)) reminder |= SpecificRemindService.ReminderType.TodayReminder; - } + //The reminder for this service is needed, check why it is needed and set the flags + if (service.GetType() == typeof(PastDayNotComplete)) reminder |= SpecificRemindService.ReminderType.YesterdayReminder; + if (service.GetType() == typeof(TimeSheetNotFullEnough)) reminder |= SpecificRemindService.ReminderType.TodayReminder; } else { - //As soon as one reminder check was negative, we return "NoReminder" since all checks needs to be true! - //TODO not always break! We can remind for yesterdays times even if it is early morning! - return SpecificRemindService.ReminderType.NoReminder; + //The reminder for this service is not needed. Therefore we check, what was negative and set the appropriate flag! + if (service.GetType() == typeof(EndOfWorkingDay)) reminder |= SpecificRemindService.ReminderType.OutOfWorkTime; + if (service.GetType() == typeof(UserDidNotSayStop)) reminder |= SpecificRemindService.ReminderType.UserSaidStop; + if (service.GetType() == typeof(NotOnLeave)) reminder |= SpecificRemindService.ReminderType.UserOnLeave; } } - return reminder; - - // foreach (var service in _services.Select(service => service.ReminderIsNeeded(profile))) - // { - // - // } - // - // bool[] conditions = await Task.WhenAll(_services.Select(service => service.ReminderIsNeeded(profile))); - // return conditions.All(c => c); } } } \ No newline at end of file diff --git a/src/Remind/SpecificRemindService.cs b/src/Remind/SpecificRemindService.cs index a7bd00e..8e2df54 100644 --- a/src/Remind/SpecificRemindService.cs +++ b/src/Remind/SpecificRemindService.cs @@ -59,7 +59,9 @@ public enum ReminderType TodayReminder = 1, YesterdayReminder = 2, WeekReminder = 4, - DicReminder = 8 + OutOfWorkTime = 8, + UserSaidStop = 16, + UserOnLeave = 32 }; From 5ec533e583b46361e76ca012dc9e37a97a97bce3 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 11:21:28 +0100 Subject: [PATCH 08/15] added params to api/timesheet/remind for specific reminding --- src/Clockify/ClockifyController.cs | 26 +++++++++++++++++---- src/Remind/SpecificRemindService.cs | 35 ++++++++++++++++------------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/Clockify/ClockifyController.cs b/src/Clockify/ClockifyController.cs index 08fa7b1..ab74af6 100644 --- a/src/Clockify/ClockifyController.cs +++ b/src/Clockify/ClockifyController.cs @@ -1,7 +1,9 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; using Bot.Remind; using Bot.Security; using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Cosmos.Linq; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; @@ -16,7 +18,8 @@ public class ClockifyController : ControllerBase private readonly IFollowUpService _followUpService; public ClockifyController(IBotFrameworkHttpAdapter adapter, - IProactiveBotApiKeyValidator proactiveBotApiKeyValidator, ISpecificRemindServiceResolver specificRemindServiceResolver, + IProactiveBotApiKeyValidator proactiveBotApiKeyValidator, + ISpecificRemindServiceResolver specificRemindServiceResolver, IFollowUpService followUpService) { _adapter = adapter; @@ -32,8 +35,23 @@ public async Task GetTimesheetRemindAsync() string apiToken = ProactiveApiKeyUtil.Extract(Request); _proactiveBotApiKeyValidator.Validate(apiToken); - return await _entryFillRemindService.SendReminderAsync(_adapter, - SpecificRemindService.ReminderType.TodayReminder | SpecificRemindService.ReminderType.YesterdayReminder); + var typesToRemind = SpecificRemindService.ReminderType.YesterdayReminder | + SpecificRemindService.ReminderType.TodayReminder; + + //Check for additional query parameters. If there are available, we will only remind those reminders + if (Request.Query.ContainsKey("type")) + { + var requestedReminderTypes = Request.Query["type"]; + //Check for the specific teminder types + typesToRemind = SpecificRemindService.ReminderType.NoReminder; + if (requestedReminderTypes.Contains("yesterday")) + typesToRemind |= SpecificRemindService.ReminderType.YesterdayReminder; + + if (requestedReminderTypes.Contains("today")) + typesToRemind |= SpecificRemindService.ReminderType.TodayReminder; + } + + return await _entryFillRemindService.SendReminderAsync(_adapter, typesToRemind); } [Route("api/follow-up")] diff --git a/src/Remind/SpecificRemindService.cs b/src/Remind/SpecificRemindService.cs index 8e2df54..d75c66e 100644 --- a/src/Remind/SpecificRemindService.cs +++ b/src/Remind/SpecificRemindService.cs @@ -99,11 +99,12 @@ private bool SendSpecificReminderType(IBotFrameworkHttpAdapter adapter, UserProf return true; } - public async Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, ReminderType reminderTypes) + public async Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, ReminderType typesToRemind) { var reminderCounter = 0; + var userCounter = 0; //Check, whether we need to remind at least one event - if (reminderTypes != ReminderType.NoReminder) + if (typesToRemind != ReminderType.NoReminder) { async Task ReminderNeeded(UserProfile u) => await _compositeNeedRemindService.ReminderIsNeeded(u); @@ -122,24 +123,28 @@ async Task ReminderNeeded(UserProfile u) => //Check if we need to remind the user if (userReminderTypes != ReminderType.NoReminder) { - //Switch between the different reminder types - switch (userReminderTypes) + userCounter++; + + //Check, if the user needs a reminder for today and if we also have requested a reminder for today. + if (userReminderTypes.HasFlag(ReminderType.TodayReminder) && + typesToRemind.HasFlag(ReminderType.TodayReminder)) { - case ReminderType.TodayReminder: - if (SendSpecificReminderType(adapter, userProfile, ReminderType.TodayReminder)) - reminderCounter++; - break; - - case ReminderType.YesterdayReminder: - if (SendSpecificReminderType(adapter, userProfile, ReminderType.YesterdayReminder)) - reminderCounter++; - break; + if (SendSpecificReminderType(adapter, userProfile, ReminderType.TodayReminder)) + reminderCounter++; + } + + //Check, if the user needs a reminder for yesterday and if we also have requested a reminder for yesterday. + if (userReminderTypes.HasFlag(ReminderType.YesterdayReminder) && + typesToRemind.HasFlag(ReminderType.YesterdayReminder)) + { + if (SendSpecificReminderType(adapter, userProfile, ReminderType.YesterdayReminder)) + reminderCounter++; } } } } - return $"Sent reminder to {reminderCounter} users"; - } + return $"Sent {reminderCounter} reminder to {userCounter} users"; + } } } \ No newline at end of file From 7b9d8b52178f14b53ebe88826af75c1357720548 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 11:37:02 +0100 Subject: [PATCH 09/15] added optional parameter "respectWorkingHours" --- src/Clockify/ClockifyController.cs | 14 ++++++++++++-- src/Remind/ISpecificRemindService.cs | 3 ++- src/Remind/SpecificRemindService.cs | 12 ++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Clockify/ClockifyController.cs b/src/Clockify/ClockifyController.cs index ab74af6..9bef3be 100644 --- a/src/Clockify/ClockifyController.cs +++ b/src/Clockify/ClockifyController.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using Bot.Remind; using Bot.Security; @@ -38,6 +39,15 @@ public async Task GetTimesheetRemindAsync() var typesToRemind = SpecificRemindService.ReminderType.YesterdayReminder | SpecificRemindService.ReminderType.TodayReminder; + bool respectWorkingHours = true; + + //Check, whether we should disturb the employee even if it is the mid of the day + if (Request.Query.ContainsKey("respectWorkingHours")) + { + if (Request.Query["respectWorkingHours"].Contains("true")) respectWorkingHours = true; + if (Request.Query["respectWorkingHours"].Contains("false")) respectWorkingHours = false; + } + //Check for additional query parameters. If there are available, we will only remind those reminders if (Request.Query.ContainsKey("type")) { @@ -51,7 +61,7 @@ public async Task GetTimesheetRemindAsync() typesToRemind |= SpecificRemindService.ReminderType.TodayReminder; } - return await _entryFillRemindService.SendReminderAsync(_adapter, typesToRemind); + return await _entryFillRemindService.SendReminderAsync(_adapter, typesToRemind, respectWorkingHours); } [Route("api/follow-up")] diff --git a/src/Remind/ISpecificRemindService.cs b/src/Remind/ISpecificRemindService.cs index aa2424d..642f6f9 100644 --- a/src/Remind/ISpecificRemindService.cs +++ b/src/Remind/ISpecificRemindService.cs @@ -5,6 +5,7 @@ namespace Bot.Remind { public interface ISpecificRemindService { - Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, SpecificRemindService.ReminderType reminderTypes); + Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, SpecificRemindService.ReminderType reminderTypes, + bool respectWorkHours); } } \ No newline at end of file diff --git a/src/Remind/SpecificRemindService.cs b/src/Remind/SpecificRemindService.cs index d75c66e..0123c1f 100644 --- a/src/Remind/SpecificRemindService.cs +++ b/src/Remind/SpecificRemindService.cs @@ -99,7 +99,8 @@ private bool SendSpecificReminderType(IBotFrameworkHttpAdapter adapter, UserProf return true; } - public async Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, ReminderType typesToRemind) + public async Task SendReminderAsync(IBotFrameworkHttpAdapter adapter, ReminderType typesToRemind, + bool respectWorkHours) { var reminderCounter = 0; var userCounter = 0; @@ -123,8 +124,15 @@ async Task ReminderNeeded(UserProfile u) => //Check if we need to remind the user if (userReminderTypes != ReminderType.NoReminder) { - userCounter++; + //Check if we are out of working hours and we also want to check for this, break. + if (userReminderTypes.HasFlag(ReminderType.OutOfWorkTime) && respectWorkHours) + { + break; + } + //Only upcount for users, which are not affected by the "OutOfWorkTime" condition + userCounter++; + //Check, if the user needs a reminder for today and if we also have requested a reminder for today. if (userReminderTypes.HasFlag(ReminderType.TodayReminder) && typesToRemind.HasFlag(ReminderType.TodayReminder)) From b89a6e8bba2cc89e5f2a579d1d589f1a36024c98 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 11:44:29 +0100 Subject: [PATCH 10/15] Changed the default behaviour for the endpoint. --- src/Clockify/ClockifyController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Clockify/ClockifyController.cs b/src/Clockify/ClockifyController.cs index 9bef3be..865e214 100644 --- a/src/Clockify/ClockifyController.cs +++ b/src/Clockify/ClockifyController.cs @@ -36,8 +36,8 @@ public async Task GetTimesheetRemindAsync() string apiToken = ProactiveApiKeyUtil.Extract(Request); _proactiveBotApiKeyValidator.Validate(apiToken); - var typesToRemind = SpecificRemindService.ReminderType.YesterdayReminder | - SpecificRemindService.ReminderType.TodayReminder; + //Only use TodayReminder as default to be compatible to the old behaviour of the endpoint + var typesToRemind = SpecificRemindService.ReminderType.TodayReminder; bool respectWorkingHours = true; From 0cfe683122ba929b635ebd89b3e5df8b539cd0ec Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 14:06:34 +0100 Subject: [PATCH 11/15] Added user specific working hours and ability to set them using "change my work hours to xx hours" --- src/Clockify/ClockifyHandler.cs | 9 +- src/Clockify/ClockifyMessageSource.cs | 2 + src/Clockify/IClockifyMessageSource.cs | 2 + src/Clockify/PastDayNotComplete.cs | 20 ++- src/Clockify/TimeSheetNotFullEnough.cs | 19 ++- src/Clockify/User/UserSettingsDialog.cs | 116 ++++++++++++++++++ src/Common/Recognizer/TimeSurveyBotLuis.cs | 3 +- .../Clockify.ClockifyMessageSource.resx | 6 + src/Startup.cs | 2 + src/States/UserProfile.cs | 2 + 10 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/Clockify/User/UserSettingsDialog.cs diff --git a/src/Clockify/ClockifyHandler.cs b/src/Clockify/ClockifyHandler.cs index cc564b7..4871b31 100644 --- a/src/Clockify/ClockifyHandler.cs +++ b/src/Clockify/ClockifyHandler.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Bot.Clockify.Fill; using Bot.Clockify.Reports; +using Bot.Clockify.User; using Bot.Common.Recognizer; using Bot.States; using Bot.Supports; @@ -15,23 +16,26 @@ public class ClockifyHandler : IBotHandler { private readonly EntryFillDialog _fillDialog; private readonly ReportDialog _reportDialog; + private readonly UserSettingsDialog _userSettingsDialog; private readonly StopReminderDialog _stopReminderDialog; private readonly ClockifySetupDialog _clockifySetupDialog; private readonly DialogSet _dialogSet; private readonly IStatePropertyAccessor _dialogState; public ClockifyHandler(EntryFillDialog fillDialog, ReportDialog reportDialog, - StopReminderDialog stopReminderDialog, ConversationState conversationState, + StopReminderDialog stopReminderDialog, UserSettingsDialog userSettingsDialog, ConversationState conversationState, ClockifySetupDialog clockifySetupDialog) { _dialogState = conversationState.CreateProperty("ClockifyDialogState"); _fillDialog = fillDialog; _reportDialog = reportDialog; + _userSettingsDialog = userSettingsDialog; _stopReminderDialog = stopReminderDialog; _clockifySetupDialog = clockifySetupDialog; _dialogSet = new DialogSet(_dialogState) .Add(_fillDialog) .Add(_stopReminderDialog) + .Add(_userSettingsDialog) .Add(_reportDialog) .Add(_clockifySetupDialog); } @@ -52,6 +56,9 @@ public async Task Handle(ITurnContext turnContext, CancellationToken cance { switch (luisResult.TopIntentWithMinScore()) { + case TimeSurveyBotLuis.Intent.SetWorkingHours: + await dialogContext.BeginDialogAsync(_userSettingsDialog.Id, luisResult, cancellationToken); + return true; case TimeSurveyBotLuis.Intent.Report: await dialogContext.BeginDialogAsync(_reportDialog.Id, luisResult, cancellationToken); return true; diff --git a/src/Clockify/ClockifyMessageSource.cs b/src/Clockify/ClockifyMessageSource.cs index 007d397..03eba2a 100644 --- a/src/Clockify/ClockifyMessageSource.cs +++ b/src/Clockify/ClockifyMessageSource.cs @@ -24,6 +24,8 @@ public ClockifyMessageSource(IStringLocalizer localizer) public string TaskCreation => GetString(nameof(TaskCreation)); public string TaskAbort => GetString(nameof(TaskAbort)); public string AddEntryFeedback => GetString(nameof(AddEntryFeedback)); + public string SetWorkingHoursFeedback => GetString(nameof(SetWorkingHoursFeedback)); + public string SetWorkingHoursUnchangedFeedback => GetString(nameof(SetWorkingHoursUnchangedFeedback)); public string EntryFillUnderstandingError => GetString(nameof(EntryFillUnderstandingError)); public string AmbiguousProjectError => GetString(nameof(AmbiguousProjectError)); public string ProjectUnrecognized => GetString(nameof(ProjectUnrecognized)); diff --git a/src/Clockify/IClockifyMessageSource.cs b/src/Clockify/IClockifyMessageSource.cs index 398a119..015ad5f 100644 --- a/src/Clockify/IClockifyMessageSource.cs +++ b/src/Clockify/IClockifyMessageSource.cs @@ -11,6 +11,8 @@ public interface IClockifyMessageSource string TaskCreation { get; } string TaskAbort { get; } string AddEntryFeedback { get; } + string SetWorkingHoursFeedback { get; } + string SetWorkingHoursUnchangedFeedback { get; } string EntryFillUnderstandingError { get; } string AmbiguousProjectError { get; } string ProjectUnrecognized { get; } diff --git a/src/Clockify/PastDayNotComplete.cs b/src/Clockify/PastDayNotComplete.cs index a301fa4..3c4d64a 100644 --- a/src/Clockify/PastDayNotComplete.cs +++ b/src/Clockify/PastDayNotComplete.cs @@ -14,6 +14,15 @@ public class PastDayNotComplete : INeedRemindService private readonly IClockifyService _clockifyService; private readonly ITokenRepository _tokenRepository; private readonly IDateTimeProvider _dateTimeProvider; + + //Get de default hours to work. If not defined, assume 8hours + public static readonly string DefaultWorkingHours = + Environment.GetEnvironmentVariable("DEFAULT_WORKING_HOURS") ?? "8"; + + //Get the minimum percentage of hours filled. If not defined, assume 75% of a default work day to be reported. + //This leads to 6 hours + public static readonly string MinimumHoursFilledPercentage = + Environment.GetEnvironmentVariable("MINIMUM_HOURS_FILLED_PERCENTAGE") ?? "75"; public PastDayNotComplete(IClockifyService clockifyService, ITokenRepository tokenRepository, IDateTimeProvider dateTimeProvider) @@ -57,7 +66,16 @@ public async Task ReminderIsNeeded(UserProfile userProfile) return 0; }); - return totalHoursInserted < 6; + + //Check if we have defined the working hours on user level. If so, calculate the minimum. + if (userProfile.WorkingHours != null) + return totalHoursInserted < + (userProfile.WorkingHours * (double.Parse(MinimumHoursFilledPercentage) / 100)); + + //Calculate the minimum amount of hours to be reported based on the defaults. + return totalHoursInserted < (double.Parse(DefaultWorkingHours) * + (double.Parse(MinimumHoursFilledPercentage) / 100)); + } catch (Exception) { diff --git a/src/Clockify/TimeSheetNotFullEnough.cs b/src/Clockify/TimeSheetNotFullEnough.cs index 2fbb9b7..6974fac 100644 --- a/src/Clockify/TimeSheetNotFullEnough.cs +++ b/src/Clockify/TimeSheetNotFullEnough.cs @@ -15,6 +15,15 @@ public class TimeSheetNotFullEnough : INeedRemindService private readonly ITokenRepository _tokenRepository; private readonly IDateTimeProvider _dateTimeProvider; + //Get de default hours to work. If not defined, assume 8hours + public static readonly string DefaultWorkingHours = + Environment.GetEnvironmentVariable("DEFAULT_WORKING_HOURS") ?? "8"; + + //Get the minimum percentage of hours filled. If not defined, assume 75% of a default work day to be reported. + //This leads to 6 hours + public static readonly string MinimumHoursFilledPercentage = + Environment.GetEnvironmentVariable("MINIMUM_HOURS_FILLED_PERCENTAGE") ?? "75"; + public TimeSheetNotFullEnough(IClockifyService clockifyService, ITokenRepository tokenRepository, IDateTimeProvider dateTimeProvider) { @@ -50,7 +59,15 @@ public async Task ReminderIsNeeded(UserProfile userProfile) return 0; }); - return totalHoursInserted < 6; + + //Check if we have defined the working hours on user level. If so, calculate the minimum. + if (userProfile.WorkingHours != null) + return totalHoursInserted < + (userProfile.WorkingHours * (double.Parse(MinimumHoursFilledPercentage) / 100)); + + //Calculate the minimum amount of hours to be reported based on the defaults. + return totalHoursInserted < (double.Parse(DefaultWorkingHours) * + (double.Parse(MinimumHoursFilledPercentage) / 100)); } catch (Exception) { diff --git a/src/Clockify/User/UserSettingsDialog.cs b/src/Clockify/User/UserSettingsDialog.cs new file mode 100644 index 0000000..f37f47f --- /dev/null +++ b/src/Clockify/User/UserSettingsDialog.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bot.Clockify.Client; +using Bot.Clockify.Fill; +using Bot.Clockify.Models; +using Bot.Common; +using Bot.Common.ChannelData.Telegram; +using Bot.Common.Recognizer; +using Bot.Data; +using Bot.States; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Bot.Clockify.User +{ + public class UserSettingsDialog : ComponentDialog + { + private readonly ITokenRepository _tokenRepository; + private readonly ITimeEntryStoreService _timeEntryStoreService; + private readonly UserState _userState; + private readonly IClockifyMessageSource _messageSource; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; + + private const string TaskWaterfall = "TaskWaterfall"; + + private const string Telegram = "telegram"; + + public UserSettingsDialog(ITimeEntryStoreService timeEntryStoreService, + UserState userState, ITokenRepository tokenRepository, + IClockifyMessageSource messageSource, IDateTimeProvider dateTimeProvider, + ILogger logger) + { + _timeEntryStoreService = timeEntryStoreService; + _userState = userState; + _tokenRepository = tokenRepository; + _messageSource = messageSource; + _dateTimeProvider = dateTimeProvider; + _logger = logger; + AddDialog(new WaterfallDialog(TaskWaterfall, new List + { + PromptForTaskAsync + })); + Id = nameof(UserSettingsDialog); + } + + private async Task PromptForTaskAsync(WaterfallStepContext stepContext, + CancellationToken cancellationToken) + { + string messageText = ""; + + var userProfile = + await StaticUserProfileHelper.GetUserProfileAsync(_userState, stepContext.Context, cancellationToken); + var tokenData = await _tokenRepository.ReadAsync(userProfile.ClockifyTokenId!); + string clockifyToken = tokenData.Value; + stepContext.Values["ClockifyTokenId"] = userProfile.ClockifyTokenId; + var luisResult = (TimeSurveyBotLuis)stepContext.Options; + + var workingMinutes = luisResult.WorkedDurationInMinutes(); + var workingHours = workingMinutes / 60; + + //Default messageText + messageText = string.Format(_messageSource.SetWorkingHoursFeedback, workingHours); + + //Check if there is a need for a change + if (userProfile.WorkingHours != null) + { + if (userProfile.WorkingHours == workingHours) + messageText = string.Format(_messageSource.SetWorkingHoursUnchangedFeedback, workingHours); + } + + //Store the working hours within the userProfile + userProfile.WorkingHours = workingHours; + + //Inform user and exit the conversation. + return await InformAndExit(stepContext, cancellationToken, messageText); + } + + + private async Task InformAndExit(DialogContext stepContext, + CancellationToken cancellationToken, string messageText) + { + string platform = stepContext.Context.Activity.ChannelId; + var ma = GetExitMessageActivity(messageText, platform); + await stepContext.Context.SendActivityAsync(ma, cancellationToken); + return await stepContext.EndDialogAsync(cancellationToken: cancellationToken); + } + + + private static IMessageActivity GetExitMessageActivity(string messageText, string platform) + { + IMessageActivity ma; + switch (platform.ToLower()) + { + case Telegram: + ma = Activity.CreateMessageActivity(); + var sendMessageParams = new SendMessageParameters(messageText, new ReplyKeyboardRemove()); + var channelData = new SendMessage(sendMessageParams); + ma.ChannelData = JsonConvert.SerializeObject(channelData); + return ma; + default: + ma = MessageFactory.Text(messageText); + ma.SuggestedActions = new SuggestedActions { Actions = new List() }; + return ma; + } + + ; + } + } +} \ No newline at end of file diff --git a/src/Common/Recognizer/TimeSurveyBotLuis.cs b/src/Common/Recognizer/TimeSurveyBotLuis.cs index f893286..7677c37 100644 --- a/src/Common/Recognizer/TimeSurveyBotLuis.cs +++ b/src/Common/Recognizer/TimeSurveyBotLuis.cs @@ -29,7 +29,8 @@ public enum Intent { Report, Thanks, Utilities_Help, - Utilities_Stop + Utilities_Stop, + SetWorkingHours }; [JsonProperty("intents")] public Dictionary Intents; diff --git a/src/Common/Resources/Clockify.ClockifyMessageSource.resx b/src/Common/Resources/Clockify.ClockifyMessageSource.resx index 60f3d31..b6bba35 100644 --- a/src/Common/Resources/Clockify.ClockifyMessageSource.resx +++ b/src/Common/Resources/Clockify.ClockifyMessageSource.resx @@ -160,4 +160,10 @@ Please don't exceed one year window You have not filled in all of your hours for yesterday. Please do so! + + Ok, I set your daily working hours to {0:0} hours. Happy working 🤖 + + + Your daily working hours were already set to {0:0} hours 😅. Nothing changed. + \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index 80b53ed..d3a67d1 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -6,6 +6,7 @@ using Bot.Clockify.Client; using Bot.Clockify.Fill; using Bot.Clockify.Reports; +using Bot.Clockify.User; using Bot.Common; using Bot.Common.Recognizer; using Bot.Data; @@ -62,6 +63,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/States/UserProfile.cs b/src/States/UserProfile.cs index 30a26c8..68b62a1 100644 --- a/src/States/UserProfile.cs +++ b/src/States/UserProfile.cs @@ -20,6 +20,8 @@ public class UserProfile public string? LastName { get; set; } public ConversationReference? ConversationReference { get; set; } + + public double? WorkingHours { get; set; } public DateTime? StopRemind { get; set; } public bool Experimental { get; set; } From 7b0d9164d5703250ff5acea1f7074b032133194b Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 17:11:12 +0100 Subject: [PATCH 12/15] Added persistent local storage for testing / development --- src/Data/InMemoryTokenRepository.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Data/InMemoryTokenRepository.cs b/src/Data/InMemoryTokenRepository.cs index aeeaa4c..2715faa 100644 --- a/src/Data/InMemoryTokenRepository.cs +++ b/src/Data/InMemoryTokenRepository.cs @@ -1,17 +1,27 @@ using System; using System.Collections.Concurrent; +using System.IO; using System.Threading.Tasks; +using Newtonsoft.Json; namespace Bot.Data { public class InMemoryTokenRepository : ITokenRepository { - private readonly ConcurrentDictionary _store = new ConcurrentDictionary(); + private ConcurrentDictionary _store = new ConcurrentDictionary(); + public Task ReadAsync(string id) { if (id == null) throw new ArgumentNullException(id); + //If local storage exists, load it! + if (File.Exists("jsonStorage.json")) + { + var jsonStorage = File.ReadAllText("jsonStorage.json"); + _store = JsonConvert.DeserializeObject>(jsonStorage); + } + if (!_store.TryGetValue(id, out var value)) { throw new TokenNotFoundException("No token has been found with id " + id); @@ -25,6 +35,8 @@ public Task WriteAsync(string value, string? id = null) string name = id ?? Guid.NewGuid().ToString(); _store.AddOrUpdate(name, value, (key, current) => value); + var jsonStorage = JsonConvert.SerializeObject(_store); + File.WriteAllText("jsonStorage.json",jsonStorage); return Task.FromResult(new TokenData(name, value)); } } From 172652730e75ca496c78e960c6114c9d83b2737e Mon Sep 17 00:00:00 2001 From: khaelys Date: Tue, 9 Nov 2021 14:45:50 +0100 Subject: [PATCH 13/15] merged add logout dialog --- src/Clockify/ClockifyHandler.cs | 17 +++- src/Clockify/ClockifyMessageSource.cs | 5 ++ src/Clockify/IClockifyMessageSource.cs | 5 ++ src/Clockify/LogoutDialog.cs | 89 +++++++++++++++++++ .../Clockify.ClockifyMessageSource.resx | 12 +++ src/Startup.cs | 1 + 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/Clockify/LogoutDialog.cs diff --git a/src/Clockify/ClockifyHandler.cs b/src/Clockify/ClockifyHandler.cs index 4871b31..620fb27 100644 --- a/src/Clockify/ClockifyHandler.cs +++ b/src/Clockify/ClockifyHandler.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Bot.Clockify.Fill; @@ -19,12 +21,15 @@ public class ClockifyHandler : IBotHandler private readonly UserSettingsDialog _userSettingsDialog; private readonly StopReminderDialog _stopReminderDialog; private readonly ClockifySetupDialog _clockifySetupDialog; + private readonly LogoutDialog _logoutDialog; private readonly DialogSet _dialogSet; private readonly IStatePropertyAccessor _dialogState; + private readonly IEnumerable _logoutIntent = new HashSet { "log out", "logout" }; + public ClockifyHandler(EntryFillDialog fillDialog, ReportDialog reportDialog, StopReminderDialog stopReminderDialog, UserSettingsDialog userSettingsDialog, ConversationState conversationState, - ClockifySetupDialog clockifySetupDialog) + ClockifySetupDialog clockifySetupDialog, LogoutDialog logoutDialog) { _dialogState = conversationState.CreateProperty("ClockifyDialogState"); _fillDialog = fillDialog; @@ -32,12 +37,14 @@ public ClockifyHandler(EntryFillDialog fillDialog, ReportDialog reportDialog, _userSettingsDialog = userSettingsDialog; _stopReminderDialog = stopReminderDialog; _clockifySetupDialog = clockifySetupDialog; + _logoutDialog = logoutDialog; _dialogSet = new DialogSet(_dialogState) .Add(_fillDialog) .Add(_stopReminderDialog) .Add(_userSettingsDialog) .Add(_reportDialog) - .Add(_clockifySetupDialog); + .Add(_clockifySetupDialog) + .Add(_logoutDialog); } public async Task Handle(ITurnContext turnContext, CancellationToken cancellationToken, @@ -51,6 +58,12 @@ public async Task Handle(ITurnContext turnContext, CancellationToken cance var dialogContext = await _dialogSet.CreateContextAsync(turnContext, cancellationToken); if (await RunClockifySetupIfNeeded(turnContext, cancellationToken, userProfile)) return true; + + if (_logoutIntent.Contains(turnContext.Activity.Text)) + { + await dialogContext.BeginDialogAsync(_logoutDialog.Id, cancellationToken: cancellationToken); + return true; + } try { diff --git a/src/Clockify/ClockifyMessageSource.cs b/src/Clockify/ClockifyMessageSource.cs index 03eba2a..0965c74 100644 --- a/src/Clockify/ClockifyMessageSource.cs +++ b/src/Clockify/ClockifyMessageSource.cs @@ -42,6 +42,11 @@ public ClockifyMessageSource(IStringLocalizer localizer) public string ReportDateRangeExceedOneYear => GetString(nameof(ReportDateRangeExceedOneYear)); public string FollowUp => GetString(nameof(FollowUp)); + + public string LogoutPrompt => GetString(nameof(LogoutPrompt)); + public string LogoutYes => GetString(nameof(LogoutYes)); + public string LogoutNo => GetString(nameof(LogoutNo)); + public string LogoutRetryPrompt => GetString(nameof(LogoutRetryPrompt)); public string RemindStoppedAlready => GetString(nameof(RemindStoppedAlready)); public string RemindStopAnswer => GetString(nameof(RemindStopAnswer)); diff --git a/src/Clockify/IClockifyMessageSource.cs b/src/Clockify/IClockifyMessageSource.cs index 015ad5f..c39807b 100644 --- a/src/Clockify/IClockifyMessageSource.cs +++ b/src/Clockify/IClockifyMessageSource.cs @@ -35,5 +35,10 @@ public interface IClockifyMessageSource string RemindEntryFillYesterday { get; } string FollowUp { get; } + + string LogoutPrompt { get; } + string LogoutYes { get; } + string LogoutNo { get; } + string LogoutRetryPrompt { get; } } } \ No newline at end of file diff --git a/src/Clockify/LogoutDialog.cs b/src/Clockify/LogoutDialog.cs new file mode 100644 index 0000000..632c443 --- /dev/null +++ b/src/Clockify/LogoutDialog.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Bot.States; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; + +namespace Bot.Clockify +{ + public class LogoutDialog : ComponentDialog + { + private const string LogoutWaterfall = nameof(LogoutWaterfall); + private readonly UserState _userState; + private readonly IClockifyMessageSource _messageSource; + + private const string Yes = "yes"; + private const string No = "no"; + + public LogoutDialog(UserState userState, IClockifyMessageSource messageSource) + { + _userState = userState; + _messageSource = messageSource; + AddDialog(new WaterfallDialog(LogoutWaterfall, new List + { + ConfirmationStep, + LogoutStep + })); + AddDialog(new TextPrompt(nameof(ConfirmationStep), LogoutValidator)); + Id = nameof(LogoutDialog); + } + + private async Task ConfirmationStep(WaterfallStepContext stepContext, + CancellationToken cancellationToken) + { + var suggestions = new List + { + new CardAction + { + Title = Yes, Type = ActionTypes.MessageBack, Text = Yes, Value = Yes, + DisplayText = Yes + }, + new CardAction + { + Title = No, Type = ActionTypes.MessageBack, Text = No, Value = No, + DisplayText = No + } + }; + var activity = MessageFactory.Text(_messageSource.LogoutPrompt); + activity.SuggestedActions = new SuggestedActions { Actions = suggestions }; + return await stepContext.PromptAsync(nameof(ConfirmationStep), new PromptOptions + { + Prompt = activity, + RetryPrompt = MessageFactory.Text(_messageSource.LogoutRetryPrompt), + }, cancellationToken); + } + + private async Task LogoutStep(WaterfallStepContext stepContext, + CancellationToken cancellationToken) + { + var result = stepContext.Result.ToString(); + switch (result?.ToLower()) + { + case Yes: + var userProfile = + await StaticUserProfileHelper.GetUserProfileAsync(_userState, stepContext.Context, + cancellationToken); + userProfile.ClockifyTokenId = null; + await stepContext.Context.SendActivityAsync( + MessageFactory.Text(_messageSource.LogoutYes), cancellationToken); + break; + case No: + await stepContext.Context.SendActivityAsync(MessageFactory.Text(_messageSource.LogoutNo), + cancellationToken); + break; + } + + return await stepContext.EndDialogAsync(null, cancellationToken); + } + + private static Task LogoutValidator(PromptValidatorContext promptContext, + CancellationToken cancellationToken) + { + string? pValue = promptContext.Recognized.Value; + return Task.FromResult(!string.IsNullOrEmpty(pValue) && + (pValue.ToLower() == Yes || pValue.ToLower() == No)); + } + } +} \ No newline at end of file diff --git a/src/Common/Resources/Clockify.ClockifyMessageSource.resx b/src/Common/Resources/Clockify.ClockifyMessageSource.resx index b6bba35..9fec44b 100644 --- a/src/Common/Resources/Clockify.ClockifyMessageSource.resx +++ b/src/Common/Resources/Clockify.ClockifyMessageSource.resx @@ -166,4 +166,16 @@ Please don't exceed one year window Your daily working hours were already set to {0:0} hours 😅. Nothing changed. + + Are you sure? + + + You have successfully logged out of Clockify! + + + As you wish! + + + Please answer with 'Yes' or 'No' + \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index d3a67d1..51d4851 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -64,6 +64,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); From fdfcb7426e09fa39e794658066c987acdcf779b2 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 23:01:30 +0100 Subject: [PATCH 14/15] added secret removal function --- src/Clockify/ClockifyHandler.cs | 1 + src/Data/ITokenRepository.cs | 9 ++++++++ src/Data/InMemoryTokenRepository.cs | 34 ++++++++++++++++++++++++++--- src/Data/TokenRepository.cs | 33 ++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/Clockify/ClockifyHandler.cs b/src/Clockify/ClockifyHandler.cs index 620fb27..1ca783c 100644 --- a/src/Clockify/ClockifyHandler.cs +++ b/src/Clockify/ClockifyHandler.cs @@ -59,6 +59,7 @@ public async Task Handle(ITurnContext turnContext, CancellationToken cance if (await RunClockifySetupIfNeeded(turnContext, cancellationToken, userProfile)) return true; + //Check for fixed intents without using LUIS if (_logoutIntent.Contains(turnContext.Activity.Text)) { await dialogContext.BeginDialogAsync(_logoutDialog.Id, cancellationToken: cancellationToken); diff --git a/src/Data/ITokenRepository.cs b/src/Data/ITokenRepository.cs index 39d803d..b089d79 100644 --- a/src/Data/ITokenRepository.cs +++ b/src/Data/ITokenRepository.cs @@ -11,6 +11,15 @@ public interface ITokenRepository /// The token value /// The token could not be found. Task ReadAsync(string id); + + + ///

+ /// Removes the token data starting from a string identifier + /// + /// The token identifier + /// a boolean success indicator + /// The token could not be found. + Task RemoveAsync(string id); /// diff --git a/src/Data/InMemoryTokenRepository.cs b/src/Data/InMemoryTokenRepository.cs index 2715faa..b1537fa 100644 --- a/src/Data/InMemoryTokenRepository.cs +++ b/src/Data/InMemoryTokenRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Newtonsoft.Json; @@ -9,8 +10,23 @@ namespace Bot.Data public class InMemoryTokenRepository : ITokenRepository { private ConcurrentDictionary _store = new ConcurrentDictionary(); - + + private bool SaveStorageToFile() + { + var jsonStorage = JsonConvert.SerializeObject(_store); + try + { + File.WriteAllText("jsonStorage.json",jsonStorage); + } + catch (Exception e) + { + throw new Exception("Error during writing of local storage with message: "+ e.Message); + } + + return true; + } + public Task ReadAsync(string id) { if (id == null) throw new ArgumentNullException(id); @@ -29,14 +45,26 @@ public Task ReadAsync(string id) return Task.FromResult(new TokenData(id, value)); } + public Task RemoveAsync(string id) + { + if (!_store.TryGetValue(id, out var _)) + { + throw new TokenNotFoundException("No token has been found with id " + id); + } + + //Removes the key from the store. + _store.Remove(id, out _); + SaveStorageToFile(); + return Task.FromResult(true); + } + public Task WriteAsync(string value, string? id = null) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(value); string name = id ?? Guid.NewGuid().ToString(); _store.AddOrUpdate(name, value, (key, current) => value); - var jsonStorage = JsonConvert.SerializeObject(_store); - File.WriteAllText("jsonStorage.json",jsonStorage); + SaveStorageToFile(); return Task.FromResult(new TokenData(name, value)); } } diff --git a/src/Data/TokenRepository.cs b/src/Data/TokenRepository.cs index 9f8f461..809481a 100644 --- a/src/Data/TokenRepository.cs +++ b/src/Data/TokenRepository.cs @@ -44,10 +44,38 @@ public async Task ReadAsync(string id) } } + public async Task RemoveAsync(string id) + { + if (id == null) throw new ArgumentNullException(id); + + //First check, whether we have the key locally stored within our cache. If so, remove it! + if (_cache.TryGetValue(id, out var _)) + { + _cache.Remove(id); + } + + //Next start to delete the secret from our key vault. + try + { + await _secretClient.StartDeleteSecretAsync(id); + } + catch (RequestFailedException e) + { + if (e.Status == 404) + { + throw new TokenNotFoundException("No token has been found with id " + id); + } + + throw; + } + + return true; + } + public async Task WriteAsync(string value, string? id = null) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(value); - + string name = id ?? Guid.NewGuid().ToString(); KeyVaultSecret secret = await _secretClient.SetSecretAsync(name, value); return CacheAndGetTokenData(secret); @@ -56,7 +84,8 @@ public async Task WriteAsync(string value, string? id = null) private TokenData CacheAndGetTokenData(KeyVaultSecret secret) { var tokenData = new TokenData(secret.Name, secret.Value); - _cache.Set(tokenData.Id, tokenData, new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(_cacheSeconds) }); + _cache.Set(tokenData.Id, tokenData, + new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(_cacheSeconds) }); return tokenData; } } From 760fcaa5cbc2b983956c2b0a2a3c374fcd3361e3 Mon Sep 17 00:00:00 2001 From: Claudio Hediger Date: Mon, 27 Dec 2021 23:07:17 +0100 Subject: [PATCH 15/15] added token removal after confirmation --- src/Clockify/LogoutDialog.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Clockify/LogoutDialog.cs b/src/Clockify/LogoutDialog.cs index 632c443..ca912dd 100644 --- a/src/Clockify/LogoutDialog.cs +++ b/src/Clockify/LogoutDialog.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Bot.Data; using Bot.States; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; @@ -13,14 +14,16 @@ public class LogoutDialog : ComponentDialog private const string LogoutWaterfall = nameof(LogoutWaterfall); private readonly UserState _userState; private readonly IClockifyMessageSource _messageSource; + private readonly ITokenRepository _tokenRepository; private const string Yes = "yes"; private const string No = "no"; - public LogoutDialog(UserState userState, IClockifyMessageSource messageSource) + public LogoutDialog(UserState userState, IClockifyMessageSource messageSource, ITokenRepository tokenRepository) { _userState = userState; _messageSource = messageSource; + _tokenRepository = tokenRepository; AddDialog(new WaterfallDialog(LogoutWaterfall, new List { ConfirmationStep, @@ -65,6 +68,12 @@ private async Task LogoutStep(WaterfallStepContext stepContext var userProfile = await StaticUserProfileHelper.GetUserProfileAsync(_userState, stepContext.Context, cancellationToken); + + //Removes the token from the repository! This change reflects immediateley also within all caches + //and also on the remote key vault! + await _tokenRepository.RemoveAsync(userProfile.ClockifyTokenId!); + + //Now we can also remove the tokenID from the UserProfile userProfile.ClockifyTokenId = null; await stepContext.Context.SendActivityAsync( MessageFactory.Text(_messageSource.LogoutYes), cancellationToken);