EDMNI_{hNHhg1{6Ny|S4&r8Q>5Jz{ZCu~+itHo*KMH{VJE*L=}NJ9nHY
zq0U+25^}f$QzoaAsE^>RW~ks=?pi^DVX>M))PU?(49=$2Yo=N>4wMescx4+%=k8vD
z#tZc5))1%gkbCIjAGcbeunFGjU29e=4e|4)-8aPUOiuOb=N%JzS3cm22ru^NAMg&=
zE-hMIOkTb*l~Xu5!H#z6*y045vhJ`{E7@dTek2We;27?}Chro3`{f}_c+*44-|6R0
zZQyl3rwJEsLeuN6ANf9MFB%C&tEpU8KN1Td_t*FvJQ=aZ;Jk`GT1Pr8PY7C)L66>J
zQ)a(8_*LZ@*UB(LVtJ{ir|-={X7K!z%x;fp?V{&Td0+3cB5ZXCBDrJE{Znk2e;s)B
zp|P2#J6Cq*2j}|88NU`^(r+cvmdaeEY_i#TEg=_mT4g23(%v~bgz|PTKSUc=Xp;24
za@C7kWAkt~ROOY8#Ar&RF75eeEX*<$R~J+I-B>!GfC`4t^oQ%VJG4`**CKI@Ar3G6
z;06@Jr{wXu3Jq%%xC5FQ*O!CC5$GEeWrlM=jQ+0jYM8$KnzEDn<_+Ely&~p^M-p+!
zpWvY;#Z}dXVD)(K(WXz#_2u_xt3OLfG9xPAdFK*~QIsiX;?`y#X5t2j_vK{b>MFoW
z#RY*!{%{wZx{)@&sGOx-w@hDRJj%AXR*6u7*>P{&RLD+2bV#+pGnNw5@YAJn9kxS&
z^vbE<$oVi0`EE#}yhl`k;0tZ_Edmcv5Rs?gym$KOC7Ztu-hniO`k-+V?D0KWJCW%A
zg$Bo)Aq$C4ca0`S_e5Btch}(@NJ#PVL1F(Q@3w~%U*yu{_joqt{aK__$n2dvBbKQP
z%|i=Pg(3F^A{K_i9FRkU9=soKDsyJ)umZuLub`d%LM83<5G=2BpU)(yF;si>P{jfz
zSjGS`b-z^eJ!9Uy)XUrZF7k};1*CH5Wo>DngFh#gK@7_{=2UNHtD3nh#&(-zblo8T
zTRz>FjVxV($)qX4sai=u)1~gsSsAC?&&RVpTY>?SNK<983<15DKij>Z_i%uc!RnPb=EtQB6-K~
z0_^1%ThGtlV^7_f%b9BP^dnLF-DVJ>Czu73MUU2UP`KSI)rGDesk~va@ufWw)PM&_
z6D_m}EBg(4bh8Df%ADDU3o?BHakxBE777Z`ltS+`iOYQ9#&Y`EogH+3tDV2!rG1nb
z6_3zh5#`wK2q;I33LP7gpWAx5tLDU&$2dPOVoMw~$B
z)$p#n{#@_jop*OkXA-WRx9v&mtjTv?c-M21QK=@$IM3vywT_urrL=^YxE_%5k;UA4
zh+GN!r7qXm0lj#D_}}NixS&HaVNLiM`YG;G?$P7cvj-XGz!9GgY%5%>2d7{PaYvzGBsBcFTr<{gKWquwzR4bR!!)?Gl5@jQw>LPF%o`@bL
zT0S*m73KP{uH&0yjq%5nASU=88Q&lC1Kj_^`2k0k8&tPh!lhBSIgfr}T!YFkrOhQ>
z5Kbk#)2SQ$9jTCy8cHm%3Ifk4%a8I>{0lzKv?{7vXu0g$i!7RN+iJ
zt*sXf+0iFJeT=!u_?R|TY=hHHB}1M^6NuqNcM%p;I9nc!+F6HlS0A5qe=sS8t$Z=&
zHM1J&a>x|a>8x#kADf~klp~-3?p?{aOE|L#>e;10+Qxo0Zyf7#cC2r)t<8PH_nLKS
z{4Y4a7~kd0gIFmq(WnjRz~}-E7H;a|bb1@KC(G|MwDDiMUGb}!%9#}fcz6#Ge;GLS
zo$hO6^0W8-6YpJWj0P-Q@hXC-JW5VZ#^!Y^qrU?DY0(4H
zPhrZRd`al6Dj%hGaB%>q)Wow5pUITgs-Fc=-ME?Dkg)3t9C7}#t5PA3N$52Ka%!oE+%zsos-=KMLQ{Sc27)ZTid<)2m^^p
zFa8M7fxZW1bIy?0gwzY9G=*~Y++jb%jRRehPqqmp%}!Jqp5fE5%$`Ae>=PQqV(U9h
ztd<8Y5=g>sCtg?GJ;1N#_{keEwkq--nmG*2Qk+|I+`f*BS^gfCeOFiwP@Xel)KEkJ
z3h;~C{K9t%5EZjgu=@dQWfj}rVTgm9#|z!Q!5TFuhmbT)j&1Av;vRyN&qVvlsmM1FuN
z4JJt(`BUc^CAV+WuR9JFo)u#ZPCZ+fF-Z||`C-Y;ftvV$4pNrF{{`_S>QLRKZ_`rm
zsK2aAb1v&~YL&+EAAA$~OC*$+U$WIz_)cyqZy>V~_8)*=^>=UNG&JCsxE1RO
z1+q#$aybw<>dlVO&a7&=9U6o5g1X>MUc
zS9BBjeMg*4n>xQfOMOO+egtU2&Z^cogf?uhU~^GJDyThV(~}>bJJGYG!)B~@2+OUmpX_lXw-$49Y2r?S6l}jC5U7-&@Ho|-aB%~I%
zjOeH+qsgCg#!c!+ccIA36sY8e^H1c3X&|l{^2Qfkr?)22(EeNYg%0&z2+_9B9Sq<)0vTxRm>WM}db7dAbwL#Sm6nCVfNBIbuA|gnHoBGF$ifiwUW|s
zWL^Ej3;mi`FfB*1!7SQ&7=+IOY6^bag<+H(t9X6NPw%*+cB7gy!ps|ydwD9FyPvn>
zCcN59Wd#&S3h)j$h9FXwS}6IBPOVVqF8P+M5-5B4OIsrQz)zsw6~CSa?TKs`E`id=
zEkz$&0;gG1Fl!7%EQCkpZnn21@&&<=$}6rXD+5je?BBgamFLxL(}C5%wZg)h*;%;s(0Nh7uY)4ai2fC2p>8B3$DygJNn8|*q6vu9#8YD)NbARqx&p=EA%yg5TX0F8!JZmwDU
z;UVPtt38yvy#syNYOzI&_qW>@k0~o#8AWd0F+-cRgV2eyNu7u+W<*9Ve)*O~Yg}ze
zB_ng3a9wUInH@)CQn%Pfirlbw85PdXR3@^%-Wbz@G|6^Rz$J1K|U^i5}9S3b=0PJVZML+#KHqDH5lq?$3Ih
zOg?8%LKX^>64R>gl?GgYuz5CmYbUt61}1T=uXm9@6Azro4H)~#9P!`Z|(yMqjP`W)^=~~t1`;`h>
z5HX-OBc?khxHzS=kQO0L@~lhTy)1ENs@)j6nw!W22%5V=N&oB}cSi?rxwW-hY{Mwh
zxy2vS+*-(I10v9>d
z-$&dCLSG1w8uMEkB3;MTExhQR>#Jn~ZEUJen@8$Po(`k$4R-4Bmf(#?8Du;l4|>z}
z4cuM~%M(K>1j`m?`)u%%@~?Rs*UevZ7zAlY(txflF}?tfo#
zTY`H~)?SR#`$|jKM-Nw$QR2DheV6AL#>RGzZnJvDe&|5Jj~nl?P(T)!*^J#tV;l|2
z`@(px+Pplurs&sUr2B*>ftkt-q3=n?LeRXNljzw^W)h~tt6q(&X%t>NMcI05|&-B
zJ_C}km$a@gyd93`&PW16>44=u7O|Zy4F^e5h_BKi^F+O|gC+>j@Se}zjTV0pATx3n
zorN$iy8D2u?rKTTP2URO0=v?u32IJTj_HE7GX^<^Qf1E@B%3|>0#
znAN0pIV+h|Lyr+k>=8sQI+JVb6?W@yTANy>2-Azj+KT3hq-t|Y2L%h)rZKzQgQb?|
z7&BojZO$3ur?*$!9myEc+w!X~`iw?Ki!S@{@dVD)5i5NqJhgX&xLpmL6S@uygbkOy
zLCLu`*F`fmmZRXGc1*n)@cWq8u^%{3;agzQ?OagA;@dPcW)A%`f`1VFQDm#cnOL9x
zogL;ap1X`!dl3zhQRtej
z(*vh!qm@#%9}!#|+}O_u?%=OE7%B0r+I66A8N*j^iFcpa=P1UP0~`x4m5#1%Pug)Q
zsg#K(7r^y~$4UddcX2q`dnUQ1okyWv0>GmTZw{dI?#+pcOF)5(>MKvCj7w&qNFWX@
z&GPC(vaHNEBg(`Gv8cKGJR=RjLzr(_TzIf6WIv|JK_DTe?!?e*t(U6D+h*!R5~fXA
z8M~9ncY;rqjDE08&S)x$lTv1IVfStnxR?6OVuklYFfGWUn-2E`@c0L|+=mFyao4wk
zHp|>3)i+GWmNL^EK)no%8*}n1PNHhiX9dwnEn+_YpnCnNBh??z{QB52|a!IX_NEK;ki*uJMFgQ-$
zlNx`Wkg?yN*>Repq;cK&H+Rjqj_}qCi*z4T6uzZ#oprPje!p)w(a$KrDs08MqRgVq
zb#KLeRI+a_Lv2e7FXQv)KHs8wnMl)HLvoP(#uoC%;7R*k|M2~^Nt`w!=;{Yt{VS3k
z<om
z>*MZWEL9|fwRak?T4-DcLZ5D}fKRiJCRleMlaYSE{!G2SY!9+DEK7Fj-l$Jj(M+2>2_YzC=$iSNZ89okj%5K5$ovG
z=e84ec^fCJ2(45_eFZ_l_dEYeIL-G>RxP||7lphR<*l|nO*Xf!)BO3I!1g?km;U5AMZIFV^rz=kd7x$$gVK@c6g!X5u#ObVKHDjq45IX=WSL{
z(lsrJk=bKl+XAMJDSE+pU1Osqh4#F^3>7^?{70ARI%)wvN}h6U`igwY@HE%EMj>?1
zf1s?CE8~nH9tj=FyK_Fe9{4XGFBZ(eGx)ZaMwej6&)}RZZ|o@~LN|Q6V{)YfsUHp6
zr|$}X?uGP%`kCtFj;|X0n9CtOM?y6cB`#(v6i#G0lzMFNUK3Q^JYuWaHvddKkEfe|
zQ6r!nW~AW2sQhX0T^XA-`PnM&SirfP>{>i^jz`e8slJ?M$AbLxwmmc#hz*oA*1pn}
z5OYs5`7WG>!VhXvz07IDYJqxK1>-&&t{)P;c5aS21O(aUKDo4LcjMNu*p(q6W=(Lb
zA-dDv&!u-ur_)G(T)1Y{z95_9A(2yX*jjiW|9yssjJLzRCr+A>pqUHV;-N&sgvWRC
z1A;D4H|D27gaLS|xNwW6U8c_!I|J55f*EdW!k%TyW=xU?Pd9@7q{dpJzjMbwMk}biX(m6i=^9i+_SV-QT7fDQPy3d`f81B_E2J~9R
zg4`{>1bXR9ky^`&Y_;4uZVXd;WAVtGdMyDNht69T9pe=Q!I80ZsTvKL{$M_uRw2~-
zwdMAFVlVh?!4jL&(Lbp6X5;oKFHPhd0*l0#wD~AJA)pp-(t9B9iG7|zG`jf
zRnx>9k=CGyR&7#q6~L^yscL!m_jHuY7pld+6L3M^)*&4VJn_KF1)M^sS`JmrGF>;0
z2yjwgpe27q^Yj(~;ISCDueucJe`vQf1DFKOsLnZm&&!q#%?cCI$b5T;K9JV)GHyWm
zy;`u|*-Z%w6cO)JbCE|jNQ{A&JKrg)yC#S-`-NA&M{h_~8j)*3_ao-xOF>T5b)vO2
zM^zc}M%zy$Wd4ggP?YG5pnfe!u#wFwA}(6Btc(O@5?VGbZ|C>x?ZB0WK;L$@NFkT6
zSq{y)J2ff7Ti?IrGn-!S?cKR#gg9s>Tnf{k9z$U>&gEPE}K;xSt*9rg3knjlt$;XrS|kixT)_kUTAtJI5xg3uQ4cs
zdE%+PxE^gc;4t#RI2ku@-b`W$>QY2pvEs|AqTR=#?5MgV*ZGbgf3wK69)y=l-Gq8(
z*j+0cHRn*NP3FhXl$N
z?-p{2DiP~ruuBZyXYu}GWV$vjInj>v!H(bG84W`eyNEZNh#5_SGsV*Ku-PgJtTf{~
zF2C5VJ_~kdfJPc!3Ez3A1mSESe!F2xTl@8d#EEHxc+&jc(#PnNVgE>!H!ND2S3n;C
zyHUhvmpbJojdB8y-s3zY7H-H@1hFlwwa+K#LDCC`+S9=$07qA{&oKAw;enNo`=@_g?MzhK
zdsTaFV?4Qw*_hP^Q;XKuCv?y)!Q4YNE%J`8@
zKR-8>@0?N}vF8ljXa6yE+%<0wX^aSd-tJcj4;yN!>EyAgD$qc6?Q!Vs~|S%d68NxSe$~vi7j`N`#3?HSx4;V!0N%b$M<((+#>-
zX7=IAf}3&k?p_SGb>QQ2WNV#2^z=D$eT}FHvEI%Ug*?5m2paNXb0b&o?BdzJW2X%9p}&Pw&eqQ*K9L_Yeir&+xYD-JK_79$2Jh(Dp7$VmF?sJ`
znjMLzc!&1;j%`rX&vg%~sRpZr5&1jT8`v@wrq7UVH$RG{7@*{(n
zB;e)I7av?o4p;!s9c=3tH;XIkNKFc>1Fm%Nf{ZW9;l*V=#4+G&r{kQx&nu?g9Ot~f
z6dJ!Vm)Cj%K6sdLp)s4%(L4L_r5s+bE!LWZ8tr-WS@o;4kp*6*qB4YCd@XG9^;0
z!HvWIyu2|A#amU*W|ZD7ZH~
z(Io7&y0UwiusO#-1T@p2^=^@BY{dp)(?T7Q5*(pZU
zov>K1Jm6nV1CF+ygF8*Yu>rtE2cn@G^lF{1uWHbj`-1a{#33b#256tsG#>BS=1K~{
z>DMgKe4)6kN``;6*8E|J=U|!q57M>kC~J+Dp#x-tdcEdo{oQXX+QOehaib4F
zRvC-|;7=9Mj#jZ#cSby>(Ka&?RBmBC
zK2Lqqjk2EP)^0=oF_imP#J~Sv`f{i-163iZZj&XK8NGxlmzjRt;Z9L>ZNGU|VX5qK
zD~QYNs>^pQG#fK^?;{N=?|Ym;#pfqt=tNuaOM?oFa7%+~C9;oj*|r1*FTw*sk_0Z9
z{HDx0>*gey%9Pk0q`d~M1UtmDgI~Uvg_3x~g&e*>Ijvl06g(~nDrKc&-{P$K
zw&z_ww~t*6-!uR05vo8`?bt4YGk%RJ_KXqg(~OSOB9-+d9obf70cr>X_-405-WQu7
zCaHUuoO35u(iT|O+g+{>kg(Rct&Wa~Rai}WnGW$PF=5B&9%RsmVBDMcEzaYsLZN`x
zaSxZ9ktgTSs0v+Il~ky-Ai$kX_w>XO9Flg-mz3G3w9
z^AB!-?Ge8mv5uHH3p(N(!rgC9`&Mmsi+!-QLc|kFz5+DYw!vui6^P6djbJspB-fMaZzMtsQpR$=JJ1%e%Hd&ZP-&tBeP%GA!Thgn90Qa
z#9MpYboyF`B1n)PCBvs%d7*xWOYwr>HL}phlodB4wQ5jYU3FHgBDSL~y-}H`-QiS>
z2VWUa#C)Q`DRK1`U1b`0DI&|;%uSy3NwUvBq?+^mUtzxU#FqzaO#)ETOsW<8)8p%D
zTBD>!bm-Ws4PNn;Q*z8uUt0q&a1nF6%q(X!oph^Kh^j(iaaLsZ&Yt2Y*Sw$-DiqrH
zM<_JJH$=}Qi&MFfXZ;4(A`-;0
z1H&-R4hLmCRoBw)t?{>~W^&N)<*>XMP{i-n#D3-@;m7{pY{7TbM9ZN=|ZaWZGI
zkr!nI{(b&9`xh316YXc^dv(Rf284+X4We;{SIgkcsxI*{N+P0>f!9*_OJ)jTUCQ+j
z`|qH*n2S<57p!&ip)2h2C-v#&(@BcA?pXm03*@rHhyWoD#^<)t)!ZkT#)^x;FW?(7&
zh;mIg3)A*!K;6ehXG$yJQ-kXR%>G?7f{}X|kJy1k{;&chkjYK3V3AnTTwp;b18(RHk
zc3fms7JSvvXdZ~}_#Do@EBu+9=QGIZ8RzjX>$lK&kC%#uhXg&b5^SZ%aUF2VLzrl)W4IpbZlIBJur3s(iZ8Ne|3lL7k@<
zPv=huC_fIZ&s3YS73D=xD~G$tVQAq>BHx$fMr(9c2l?YW#`Ye6VL`uWh_-zDv->Q*
z#^77HAoi4?IuQrVmiBF~v6Tkz=`dcZrK?0uHML-jYad7rOfjLNDh-%H&k>QBw(WG#
zs6H5YN^SmYA;@p{0t&B60IN*qFdYQ>BBi%E2!xipqGWIuU`pz8v!S~!uSaGCm^tb&
zE-bv*Oso*5_K#`J0m|l_IMx4V&HhAPA)Oo_Jlg%D7MHRWE7jFQ5s5@qi=KkJvoX9N
zXLU$70dd-Y8`DkxGfZERM1nV_3m}&=vliwmQg+gf=g&kCT@l)&$&lbSoKz3^QLZJ2
z$g8nJmi4&ou5#ax&JYGz0F(ho$-`7cQ6=_W$uRnF*Y|0V<}j1n9hxp6?OmN9o-0g|
z)*9uc#206r}5%%O>&+p_yoAz@fKlFv|WC>hxG~k+7e1R#$Zh+
zkhlAme1~DNb2K%m*4YS$pdEZ=Cunx|gRsMjB`AOdld16iJM;lK)W&BX<|K6a0@D3k
z5u?oJg8tC!04iLnqx=gj-revg3-dgvG{q4~KWacB->9DjA<_ST!@BIlhs~ed-&7ODjT7V>epMV}L6X@@n>sv(@;vM=M;~2o
zlwcL(OdgESYs^sk+2@!nCsF{9gQymW+#N%2w&6X#$pP)5{gl(aT%1BsT*b{x!)@mx
z|M|2`QgSiFiC^M|Bj=_3P6Zc1RcF4xVG#M}P~6hQ+w3Ss_t*KMh)?X-OHLrTYwVe*r49
z$h|$Wx3WU={87PTz>Dt$GVPObo!S&`wH^cxB!RT~@2Q-%1OerWszInCQ~jgS0+9gZ
zT}sIkmB&!vwkw5sKt?=Kp}Mb;{qgDmE#jo$)#!p5L_&a;jA{C`V}Z2F46IT;X%lra
zPBrOl08z&%o^t~*g)}RNQaj4l4%-pUn#-}Ml=*xKy*?2Z?lfTgq^|{f2Y|Q%d4H{jtV=JXPXaL1jl-hoH`P
z`ZrVP(W|qmZTxTPaw}ctF6kC$!kf3~w*6j*<%pR6O2S(NLwnc)y=V`ry{;T9UU~}%
z<4F#59~8%i0RG6JM)l`js{31*Z7$InyHPq;8ygt4&s}NgU8d=c{WwbX;Xs2(h9E
z;dVm_(YM#a5A_jmNwNX^dEu?WCGkV7lkJ1gKaM5N)N3zVS*5pHo><|BA{Pilgz
zwy5+Q>%(~zm@f1Tevevz!q%z-Hu{HYFli*ZFPG%UVPdP7$wJ|4wEonHdau(U^F)IY
zL$uS)g}vwwCPH3RX|#m0nMtG7BJdrx>%z0FyM;QUestd4Yz`k{*KCfD@non#KF;?$
z*cqRK6+cu}PUff%3q|77C04%*EgxM{9jaq1j`x+lpBN}g_pBYJPYZ}UsYX>vF%t6`
zWUJ#)@3WlO=6}bhkfZ;D!Ng4;>J}Tf)KP=p1Rl3V^(cg2wVm};w~^+%t>@xQLBYAt
zG^t8b3Ph}HkJAabu!^QNsFAe)-h=O|Dw(CmLbEC@6Z$-H7EI%(^>~&1&L#IGsPn(w
zmHj6yM+Io34RA_LQQS;OUzH@ltp@WLQ!#lhI5axW=(E_7B!F0v`6B}BiBEv4;rjI9
zQOaiUh!c$N7e%9mpA|);8nRd}BI7p!D7$_}g9`uqisB!hyCU7s^?sJ0CT}4cKzc9Y
zzw}#Qbo%57ZhwK*sUCHCRzGX*%nP3uv&aas3q+&ijklCB1BaL~=zjW%TpSY&lsNw0
zLkgAs%J{12OYN`2g3+M2`hS*MeGTuxZ32;9p@T|&Fz&5DS;Q2ItB3xMtJ^YBTuszL
zk)}t*!?ds{{fTZKbDHMfGvYb(&TOJ-s)5@SgI=n2W=mRSXBRvokG7
zoapJBrDAl4y(j4}e4e!Oqs2{B>HK5j72^yC_5-`iDJZm}<6f5o&e?<9S_z%EE{W-_
z@X-2>xseM78fYwq^sV7ItD~VWI|E7I-8N4~@7F%$#7q`OaUJfeab3a6#RFY>vML^^
zj;)}w?z6tw;3zo;9Z;^mbQ|N)jsy
z?q08MktV=)2+s>2-gpv^rSffml89rl3AI*
zmC7KG+V#f#Z5orOUZag7j7mvA?Nc@;x*p*yfeYqUP%8m?(C_BsAO9bD2OwbpPHMRW
zW<|qXUSzbqM>8cARV6smKYj~bxJE*D*i6|K+QX`n68TDWV)a;fh{>B;-s2x?gMZkn
zpgx=_;47M;Y9OjGsCa@Ay-%C!8zItIa14hmzPJ3tG9CDq^E;@De)*T6>cxK=RNdW(
z{xhg*mywCrueQ@b$6et~^V0(Y0CS)mQeyTm&rgyiA4@TA@_MB400by{J7yH>7El4_
zKU#-}7v951;f5?oUSG0({O_68OX$&Srz0Jw*qCEA1)ELuz{N;g(ESBr-=x)
zoRGsC7Ivls#ft@D+qYB+A7;VVf-UeJL)tLYKdA?=q@^h;sJ5*!#warOF-q@g_tU15E?Txypzq`|w-!kl`uRVjsN2`TN4$G8Z4#e4>zYuL_&dMCR0A
ztixvX-W60t&28SDtn%LYk-Rx%l{gvjuFjDSGc2tYW3PFNUxo)o0uoY+lSORsA*J)6
z$}LANSEvd%auM82rxGzhQIa&lIc9mF3E?Z-uG3SIRHxuW!>dm?WS(=m|J0^X{pgl5
zuKv6*uu#)hh@x9>(Ah!mmqOQub$7gAC&ZMqiPF~xW55FL+bh_!xD@Yw^?NP;{`i%Q
zu(zC9Lte5}a}t>+$#7&_UZUF
z;^^URkUlHWquKs14K{Y*VkYjkY6Roh@|bF_VO{=-C`qy{lqs83GyAbWUaAvzwp?iz
z%Dp4mZ>7pAP^{D>CWwn>d-+Q77Xmw~Lb5w%UUf%_E#t@<=MZ;|h^aL`$0AGZ$JUFo
zx2~dhFBfM@qTgqZ=p|rgf3Z3-Vn{bwyTndyeHf{}%Cg3F^U5o#(%;
zy*S|hX~d$YH4UL0EhU*^?w%VPh%Zk_TnZU2cGRxrCRpkK74N`8#~cq@ElC~;&{76{
z^LIgy7D@1!yW9%aPbHe9~OXSR{e9txt`hI4f;Z;
z(WhuwxA1n7u*1=0f9QAjD!s9-Ht~2Xzr{FA-2gsLVs=z9fbd__94a`tco^01O$Xu=
z=?I9b8l>Ht&E)bY;frbv2yt9vzg>0Qnh1<+B(o9?cpOd}>3HBz!FNZKe!A9BEU29~
znJzb7@3r;#ek+sJiyKxc3&(EI@-4Dd=_R_OLjwUpCEX9sdp-|W~P
z-%4xg10B95hFJjpUESTYID8ywQ)RJH8>0BM^x-|z$L`$r_1x!!qG4s%uMCr}UgEI31O=C1AHo$*mHM2bwB%4c<1jZivuLxV5-er5N5TTqKFIthR
zR|%+HU7$+lknK_JCcB6?zil8?>@$RmRr+0F`r;N!fufpOs10AJP}8+=Uy@l%PGdr=
zsh7bzovV=G?Oi&czX$Cv((h*hP}lr2Q9mX||GF^>Oxf0Y_!ztP9^DMk7kzDql--9u
zXpabV*CZi(7)|>at3EcG+lYBqlc=NaPi2{q92ua86gwk6m0ew$?sC>ivV{lXg&{N4
z2D3>+jtT;J+V}Yx*e|_IT3jUfWrb45eoxCko3nErSjeTf@qF))@~7mB!&B3}k(l~P
zBsAijERr}3psG|rB9zYGwCq3Nd7t6@&s&Xz&&a6v7>EkvW(YO6M!L4Oz|k%G;8f@i
zq+!}>v>y^)eOx}UD(0~bNXel3Lh_yV72N*`_;sMixa8{|cg;NtWP1h`{O_WZUFZ#4
z>pn|=?lMC212qSRM%ujbaGdrFX?)j-B-iVpL(6UB*{#cTHta0wkuwNm~dw;Uog+!v-f`2UVE*dD7}6$*vodv>a6G~5Bxeo)w!4-gfK9QtZX@<i_ZLD!l9P~o;oRBhw>h$O%He1Z(|VD~f?2uXX6h@gbeG=bGB3>)GNrt~mxOZv
zA_*xoV(@-!ourbGDHS`uFGjh+EzDq3MdU~y6l6w_Jim?h*#&
zM|i{w*G@c>H+)-yjI%GxH^IN*)hBP1NmcR_tfCcJ>iU4P_bUg41V#pu?j@~ejaNYH
zRRxaz5POvvK)SE+^Ln%!X+Z3R?o^u&^~Ae1`Y)^oIo1GF|~Fe_woNh7XF(VuF8OX>LEnRi>6Y$5MYCqxIM2sAFw>nGulzb
zDxFR=?BnCeJ2B;%rugr>NtM?&b+8G_fazB`iTmn(9)+=C^UG|}a~b)67l^beofP;m
zS=qu=SEd=Su<4kCMtfGSTAZaBJvEOU9Ot?g*USkis$m1%q(2O4i9ZbKXpxZk<{#qK
zbGLr)m5Cc@|J6@ou)>=!Sf2T7KS^H^;;ImJl6a@>l&owGu*0(4bjYcWT*!7bGayBB4N2UuGwe>c^X
zxXxN}G0(lls9pN7s}R<^8|QPT4($12rD_q1Pcd5gH-)8Yx?d?Qs9H;0K%cob)4ZB!
zS{QmG@M#T6A14-k>79Bp5pcOUxc&JL=X4Loaqo|H3mn~R^c@p+IsL*i56kcOyO~n4
zCvoaLv*ex73=)h1xo~AuO6Eg~V+6cn8t3u)eOzF+X?ovv(%YOQL(W+4v0?vBTuI^l
zewE6X!!o@A$I>>Ii#Z;04sTlh$KNvRoz$=*TCSWto$)wl9PuYh?z0o!ONBukHzcQi
zLR>#TkZc}DZhQqfqGspg#6+SFwM76ov0Q{AB{H
zx1-pFBC5xVSlDLl&&&JI_gL$thTPsY#u3BrEO&5tXJ$$_njRG^0VC(Ovo(@hDN?Q!
z<704~_EU%rkP*M6j64FFBNO|2NgjvQ_L6)LT?~)|Ho@
z)F~v*nG1gwfC5-R-Ky6=3(>t6f)Aa6?8UhCtOvhB9X$Y;I_IS{`K9Up_e{S3kcHEA
zh=C!hN3=4LMuoe=18KX5I4W!s=wRM`7)`Oqmv!ugviSVwAZkn3PDCh4Gp{d@3d(IS
z90`xIeSUfVB)YpJ#f!ZdN+36G+8XEKFy3v#g$~Yf?F<_EKv*+0oY=AiZQ(F~m#HX$
zPQZMpEn4EGi*{XTyD8KX@=j*PH}g47bdsw!9$(@#zjf;=qA9n5q{W3GS;r`3H-Ruk
zUE+i$VGqC3e*^sC^y_zengiQPivXqUFidGrHKfA&7RA0SxgJ3P>5Qe-3*rf`9+N3o
zV)Na>J|1_#C3>EU`ZoA9A(#ZE;bH3TO$D7PLx9555mOE-k{TK$8WrIKqD|oL!d~n0
zLxa6#EOxq&r2NH%>9MYF1Mx*eelkCF;A5)GAoc3+c7!942Zx?|$o8=rGxA3hP+oK0
zcBi=RrZo8UkE7eK?+(sApvI8hPBK46HzRbpjAsW)xRS6K^Gxv;E-oC9?ZKAWyDdyf
z{MBW3{P+vlTg#HQ-+j}YKEFJGJY0_aXho&}!j{PDx4Bi+BvVjq#BkOD8}3Zw>!=YS
zz&MMW<(X0w41x@x@_L;%V1apJI3mT&GyxQ58B>v2mNrY;@7pk_)__zc{N~HJSzjrQu1xydZ7E_WU)12ckVq
zEG+QxBl;4Vsab>MQ2~UmpVYKbO4{kUw2fk)+^OA>++a
zRiX0{RiWAh!pAyIVNHrsOv;jl!|y`BHX_TJ6Dt-N()?3S?_jvz_;by(Ovum~_n35?
z_rO-M<^5n$i%WwmP|J(;VPj8ka_VJsCBhMgc0+h=8%!yjpjJbIy`*I?c^D3I_8c{`
znfYjC1+kR`9onMK``kVvjAA4>Q$>2NSWd*qmop>m+iKmY?A;gMMSflYU}!moLx%11
zv4lMy_xtt7L(P3!#f=bKq0Gfm9yx-Vpo3j+eLnjyIuz{3D-vpgmEk>WE`^=X?o?D#
z2qy
zUM!_g9KK;DP90ffv3FBSs5J|+wY+YyZyh6%p^MZcrPgWIvJr4vyA;Q=bT{qT13-#g
z_*LTturi^62;ZjKP`%c(DRsa{@S
z-&rA$zxu{dNky)ORw41Q`7-42MXdwg9&jNiyIo9Pqf*}`vO=)-H6ASMdG%^cHK2dz
z0{BNgQ4@`!(fSbY`vSX#%>?78>UOccfe5H_lkehsDBPoNe+Vpw8KxExfK^@~yf!8*kY>^=$CD1$UV(9DCFhAHnol%4zkCJoH|$_QgH3
zgZT5u7b2X+448H*Je6Z%(w0
zD9FZ~_Kc|J$pJT%9i%y(gqXa@$NL=h^1N1Awi|7x_B2k1l@n0RD
zR(BeloV{sLZ{C`KBx|gmRfY+x;`SV6Y-fnPxD{B0p=5i38wsDzMacUB^V)2haWX1z
zQNf{RhGlz8d3UaJyY`6;?$=qlK+JbnlrvSjUNy?bv7cq~+2EHr@blFg(wkEi}gQ6JL`D;}^AltP)R`!?bm#Xgs10
zRVSJWdL3%ecwqeDe85^$_HwPt(!Va5#;S<#kX3Oma28}di8px)BJ{q>>bH^h6pPZBg0eNrW?N`+eZGsG&8%U6-a=pu+=i
ziW1)BaTu13o30^_T3RXdHNaG$-jH(eMng@q^#b?5$x!y#);^yzc;8COS7^5(H52Td
z%rD?v%G!>(E&Qp0`Ee6S+$yU_ioRN$Y1L_<7aj8347
zWT)X^4K+&RIo$8PQrTiRFa^gi-YUYY%zP)9?(1|-XOe|ubzep)Ro~my99vfGl8tBr
z6}4=YDE&EfxU!;%d_+0dkM5{)5i?McGKWs02`XFCguUch{iuG+yjG{2>m!_DzimyV
zz6pE2ofJ>H=rSV=;+w_YIIsZjn6M}}ny5o%=nEWjcRESQv4Mbe##TQ@Fj$7y
zqpKX8Wv8H~Qg+OW9WU^7g{;`cGL~~o96S*7)$QYh$1i(yxP`hV*w?pob!6QP!8ew!
zkFLpX*+=)1b2rfk)%pVoS&mmGfL9A2a2@5C8sU>4mmId~3z!
z;Qo*@Mae!oYpn#b;60XVunT&q&Jx@m4K49>3rGl5c<9IZS%^dcw=!5#TY>CRX`BZd1K7f=n3XL0R*<;>XCEeUI?l-JKoXC(yTQV`~y3u5q
zVsr``(rF;^8MDB9S!i8oX+oCih0Zu-@aW|kJ2v2_UtI?8jel~iSEDaN^9?auGyD)R
zI4*(%A9=qJl9JCMJ=hDBMm4Im7PE2P0lbkrTl1w)SQU_Rg`bPdisf!&2pz)-2^JEK&2Nt)H@*1%u7I
zVf@c5jUQ`l}HBFKP4Iey!Sk-#UnAKB5m{XG{z0rlOm+
zl9?J8azxF~tBdgjqthgG-ib=B;Jk`e@k!1XGm98ph1OS1S=5D@oW5E~N5$KNEH|xl
zb|xpdRdyz;1)kBFXqo6WXrQv&1b(zEv%T)yHD*rC2}npwLAPWaTt18
zheI3bx{0W!Sfe|E=K8aV%U|pVrz3XzI&+Px{X1*B^D-m2+jlmYSzW04m0MRE>gUNg
zrVY6K{DDbMn>wg{RDp+29P!ndcJT3Np&%Db`AF`(A-T6eu!Ukhl6<+HRo|}5`D~7d
zB9(Be-{7oIgKpNQ2&^9cNqfhS?Q4LV{vEIdXY!ip@1DYM=BP7~?Z;!U@EjwYZztvX
zSan;_ledCAf&ghxq!P5~d1d+)B7BBBb?)3zsVuD0l}`mF*_|-{_A!ljL_3T!SfY0a
z3KS5a7wWDO4iLPd@z8c6BGqo@f|Z4Df%cJ@se+9|YAy_!(lbWaP$ywd{+e6T)>2mE
zo$FDk>r92*-Hn7Q)gt>r>sDs7@cgJ5@OjMJsDs`Q!X3^0zxd<-an}K0G@k6`+>b8+
wUO70h+~}B`qtEBpCCdK)AJ(`PJ$r>ci` interface provided by MediatR, this tells MediatR to dispatch `MessageReceivedNotification` notifications to this handler class.
+
+> [!NOTE]
+> You can create as many notification handlers for the same notification as you desire. That's the beauty of MediatR!
+
+## Testing
+
+To test if we have successfully implemented MediatR, we can start up the bot and send a message to a server the bot is in. It should print out the message we defined earlier in our `MessageReceivedHandler`.
+
+
+
+## Adding more event types
+
+To add more event types you can follow these steps:
+
+1. Create a new notification class for the event. it should contain all of the parameters that the event would send. (Ex: the `MessageReceived` event takes one `SocketMessage` as an argument. The notification class should also map this argument)
+2. Register the event in your `DiscordEventListener` class.
+3. Create a notification handler for your new notification.
diff --git a/docs/guides/other_libs/samples/MediatrConfiguringDI.cs b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs
new file mode 100644
index 000000000..3bef7bd76
--- /dev/null
+++ b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs
@@ -0,0 +1 @@
+.AddMediatR(typeof(Bot))
diff --git a/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs
new file mode 100644
index 000000000..449c96eb4
--- /dev/null
+++ b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs
@@ -0,0 +1,16 @@
+// MessageReceivedNotification.cs
+
+using Discord.WebSocket;
+using MediatR;
+
+namespace MediatRSample.Notifications;
+
+public class MessageReceivedNotification : INotification
+{
+ public MessageReceivedNotification(SocketMessage message)
+ {
+ Message = message ?? throw new ArgumentNullException(nameof(message));
+ }
+
+ public SocketMessage Message { get; }
+}
diff --git a/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs
new file mode 100644
index 000000000..09583c3e9
--- /dev/null
+++ b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs
@@ -0,0 +1,46 @@
+// DiscordEventListener.cs
+
+using Discord.WebSocket;
+using MediatR;
+using MediatRSample.Notifications;
+using Microsoft.Extensions.DependencyInjection;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediatRSample;
+
+public class DiscordEventListener
+{
+ private readonly CancellationToken _cancellationToken;
+
+ private readonly DiscordSocketClient _client;
+ private readonly IServiceScopeFactory _serviceScope;
+
+ public DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope)
+ {
+ _client = client;
+ _serviceScope = serviceScope;
+ _cancellationToken = new CancellationTokenSource().Token;
+ }
+
+ private IMediator Mediator
+ {
+ get
+ {
+ var scope = _serviceScope.CreateScope();
+ return scope.ServiceProvider.GetRequiredService();
+ }
+ }
+
+ public async Task StartAsync()
+ {
+ _client.MessageReceived += OnMessageReceivedAsync;
+
+ await Task.CompletedTask;
+ }
+
+ private Task OnMessageReceivedAsync(SocketMessage arg)
+ {
+ return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
+ }
+}
diff --git a/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs
new file mode 100644
index 000000000..1ab2491e2
--- /dev/null
+++ b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs
@@ -0,0 +1,17 @@
+// MessageReceivedHandler.cs
+
+using System;
+using MediatR;
+using MediatRSample.Notifications;
+
+namespace MediatRSample;
+
+public class MessageReceivedHandler : INotificationHandler
+{
+ public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})");
+
+ // Your implementation
+ }
+}
diff --git a/docs/guides/other_libs/samples/MediatrStartListener.cs b/docs/guides/other_libs/samples/MediatrStartListener.cs
new file mode 100644
index 000000000..72a54bf25
--- /dev/null
+++ b/docs/guides/other_libs/samples/MediatrStartListener.cs
@@ -0,0 +1,4 @@
+// Program.cs
+
+var listener = services.GetRequiredService();
+await listener.StartAsync();
diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml
index b1a6b4721..af0a8e2b4 100644
--- a/docs/guides/toc.yml
+++ b/docs/guides/toc.yml
@@ -115,6 +115,8 @@
topicUid: Guides.OtherLibs.Serilog
- name: EFCore
topicUid: Guides.OtherLibs.EFCore
+ - name: MediatR
+ topicUid: Guides.OtherLibs.MediatR
- name: Emoji
topicUid: Guides.Emoji
- name: Voice
diff --git a/samples/MediatRSample/MediatRSample.sln b/samples/MediatRSample/MediatRSample.sln
new file mode 100644
index 000000000..d0599ae26
--- /dev/null
+++ b/samples/MediatRSample/MediatRSample.sln
@@ -0,0 +1,16 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediatRSample", "MediatRSample\MediatRSample.csproj", "{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/samples/MediatRSample/MediatRSample/DiscordEventListener.cs b/samples/MediatRSample/MediatRSample/DiscordEventListener.cs
new file mode 100644
index 000000000..dec342773
--- /dev/null
+++ b/samples/MediatRSample/MediatRSample/DiscordEventListener.cs
@@ -0,0 +1,48 @@
+using Discord.WebSocket;
+using MediatR;
+using MediatRSample.Notifications;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MediatRSample;
+
+public class DiscordEventListener
+{
+ private readonly CancellationToken _cancellationToken;
+
+ private readonly DiscordSocketClient _client;
+ private readonly IServiceScopeFactory _serviceScope;
+
+ public DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope)
+ {
+ _client = client;
+ _serviceScope = serviceScope;
+ _cancellationToken = new CancellationTokenSource().Token;
+ }
+
+ private IMediator Mediator
+ {
+ get
+ {
+ var scope = _serviceScope.CreateScope();
+ return scope.ServiceProvider.GetRequiredService();
+ }
+ }
+
+ public Task StartAsync()
+ {
+ _client.Ready += OnReadyAsync;
+ _client.MessageReceived += OnMessageReceivedAsync;
+
+ return Task.CompletedTask;
+ }
+
+ private Task OnMessageReceivedAsync(SocketMessage arg)
+ {
+ return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
+ }
+
+ private Task OnReadyAsync()
+ {
+ return Mediator.Publish(ReadyNotification.Default, _cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs b/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs
new file mode 100644
index 000000000..5cae3f267
--- /dev/null
+++ b/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using MediatRSample.Notifications;
+
+namespace MediatRSample.Handlers;
+
+public class MessageReceivedHandler : INotificationHandler
+{
+ public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})");
+
+ // Your implementation
+ }
+}
\ No newline at end of file
diff --git a/samples/MediatRSample/MediatRSample/MediatRSample.csproj b/samples/MediatRSample/MediatRSample/MediatRSample.csproj
new file mode 100644
index 000000000..4e9d01c8c
--- /dev/null
+++ b/samples/MediatRSample/MediatRSample/MediatRSample.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ Linux
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs b/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs
new file mode 100644
index 000000000..610b4a0a5
--- /dev/null
+++ b/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs
@@ -0,0 +1,14 @@
+using Discord.WebSocket;
+using MediatR;
+
+namespace MediatRSample.Notifications;
+
+public class MessageReceivedNotification : INotification
+{
+ public MessageReceivedNotification(SocketMessage message)
+ {
+ Message = message ?? throw new ArgumentNullException(nameof(message));
+ }
+
+ public SocketMessage Message { get; }
+}
\ No newline at end of file
diff --git a/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs b/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs
new file mode 100644
index 000000000..bafa6c10b
--- /dev/null
+++ b/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs
@@ -0,0 +1,13 @@
+using MediatR;
+
+namespace MediatRSample.Notifications;
+
+public class ReadyNotification : INotification
+{
+ public static readonly ReadyNotification Default
+ = new();
+
+ private ReadyNotification()
+ {
+ }
+}
\ No newline at end of file
diff --git a/samples/MediatRSample/MediatRSample/Program.cs b/samples/MediatRSample/MediatRSample/Program.cs
new file mode 100644
index 000000000..96b393e5d
--- /dev/null
+++ b/samples/MediatRSample/MediatRSample/Program.cs
@@ -0,0 +1,73 @@
+using Discord;
+using Discord.Interactions;
+using Discord.WebSocket;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
+using Serilog.Events;
+
+namespace MediatRSample;
+
+public class Bot
+{
+ private static ServiceProvider ConfigureServices()
+ {
+ return new ServiceCollection()
+ .AddMediatR(typeof(Bot))
+ .AddSingleton(new DiscordSocketClient(new DiscordSocketConfig
+ {
+ AlwaysDownloadUsers = true,
+ MessageCacheSize = 100,
+ GatewayIntents = GatewayIntents.AllUnprivileged,
+ LogLevel = LogSeverity.Info
+ }))
+ .AddSingleton()
+ .AddSingleton(x => new InteractionService(x.GetRequiredService()))
+ .BuildServiceProvider();
+ }
+
+ public static async Task Main()
+ {
+ await new Bot().RunAsync();
+ }
+
+ private async Task RunAsync()
+ {
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Verbose()
+ .Enrich.FromLogContext()
+ .WriteTo.Console()
+ .CreateLogger();
+
+ await using var services = ConfigureServices();
+
+ var client = services.GetRequiredService();
+ client.Log += LogAsync;
+
+ var listener = services.GetRequiredService();
+ await listener.StartAsync();
+
+ await client.LoginAsync(TokenType.Bot, "YOUR_TOKEN_HERE");
+ await client.StartAsync();
+
+ await Task.Delay(Timeout.Infinite);
+ }
+
+ private static Task LogAsync(LogMessage message)
+ {
+ var severity = message.Severity switch
+ {
+ LogSeverity.Critical => LogEventLevel.Fatal,
+ LogSeverity.Error => LogEventLevel.Error,
+ LogSeverity.Warning => LogEventLevel.Warning,
+ LogSeverity.Info => LogEventLevel.Information,
+ LogSeverity.Verbose => LogEventLevel.Verbose,
+ LogSeverity.Debug => LogEventLevel.Debug,
+ _ => LogEventLevel.Information
+ };
+
+ Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message);
+
+ return Task.CompletedTask;
+ }
+}
From d3a532f0132f7e35115811d938f2abc90fd68c8d Mon Sep 17 00:00:00 2001
From: Quin Lynch
Date: Tue, 5 Apr 2022 15:20:57 -0300
Subject: [PATCH 13/74] update build overrides url
---
experiment/Discord.Net.BuildOverrides/BuildOverrides.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
index fd15e5728..54b56cc60 100644
--- a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
+++ b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
@@ -130,7 +130,7 @@ namespace Discord
{
using (var client = new HttpClient())
{
- var result = await client.GetAsync($"{ApiUrl}/override/{name}");
+ var result = await client.GetAsync($"{ApiUrl}/overrides/{name}");
if (result.IsSuccessStatusCode)
{
@@ -184,7 +184,7 @@ namespace Discord
using (var client = new HttpClient())
{
- var result = await client.GetAsync($"{ApiUrl}/override/download/{ovrride.Id}");
+ var result = await client.GetAsync($"{ApiUrl}/overrides/download/{ovrride.Id}");
if (!result.IsSuccessStatusCode)
return false;
@@ -260,7 +260,7 @@ namespace Discord
{
using(var client = new HttpClient())
{
- var result = await client.PostAsync($"{ApiUrl}/override/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json"));
+ var result = await client.PostAsync($"{ApiUrl}/overrides/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json"));
if (!result.IsSuccessStatusCode)
throw new Exception("Failed to get dependency");
From 99928747032f1bc72d33d3b0b8dbb28d969a1625 Mon Sep 17 00:00:00 2001
From: Quin Lynch
Date: Tue, 5 Apr 2022 16:21:33 -0300
Subject: [PATCH 14/74] meta: 3.5.0
---
CHANGELOG.md | 26 +++++++++++++
Discord.Net.targets | 2 +-
docs/docfx.json | 2 +-
src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++---------------
4 files changed, 59 insertions(+), 33 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6884d3564..3e4de065c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
# Changelog
+## [3.5.0] - 2022-04-05
+
+### Added
+- #2204 Added config option for bidirectional formatting of usernames (e38104b)
+- #2210 Add a way to remove type readers from the interaction/command service. (7339945)
+- #2213 Add global interaction post execution event. (a744948)
+- #2223 Add ban pagination support (d8757a5)
+- #2201 Add missing interface methods to IComponentInteraction (741ed80)
+- #2226 Add an action delegate parameter to `RespondWithModalAsync()` for modifying the modal (d2118f0)
+- #2227 Add RespondWithModal methods to RestInteractinModuleBase (1c680db)
+
+### Fixed
+- #2168 Fix Integration model from GuildIntegration and added INTEGRATION gateway events (305d7f9)
+- #2187 Fix modal response failing (d656722)
+- #2188 Fix serialization error on thread creation timestamp. (d48a7bd)
+- #2209 Fix GuildPermissions.All not including newer permissions (91d8fab)
+- #2219 Fix ShardedClients not pushing PresenceUpdates (c4131cf)
+- #2225 Fix GuildMemberUpdated cacheable `before` entity being incorrect (bfd0d9b)
+- #2217 Fix gateway interactions not running without bot scope. (8522447)
+
+### Misc
+- #2193 Update GuildMemberUpdated comment regarding presence (82473bc)
+- #2206 Fixed typo (c286b99)
+- #2216 Fix small typo in modal example (0439437)
+- #2228 Correct minor typo (d1cf1bf)
+
## [3.4.1] - 2022-03-9
### Added
diff --git a/Discord.Net.targets b/Discord.Net.targets
index 187ff9d75..e50e6eceb 100644
--- a/Discord.Net.targets
+++ b/Discord.Net.targets
@@ -1,6 +1,6 @@
- 3.4.1
+ 3.5.0
latest
Discord.Net Contributors
discord;discordapp
diff --git a/docs/docfx.json b/docs/docfx.json
index 3b7ef582b..2a4ee2867 100644
--- a/docs/docfx.json
+++ b/docs/docfx.json
@@ -60,7 +60,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
- "_appFooter": "Discord.Net (c) 2015-2022 3.4.1",
+ "_appFooter": "Discord.Net (c) 2015-2022 3.5.0",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"
diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec
index 996e9bae9..d79e9a24a 100644
--- a/src/Discord.Net/Discord.Net.nuspec
+++ b/src/Discord.Net/Discord.Net.nuspec
@@ -2,7 +2,7 @@
Discord.Net
- 3.4.1$suffix$
+ 3.5.0$suffix$
Discord.Net
Discord.Net Contributors
foxbot
@@ -14,44 +14,44 @@
https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
From 8eec6a00acdca8a75f0352145cb3048a27ea46ea Mon Sep 17 00:00:00 2001
From: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com>
Date: Mon, 18 Apr 2022 02:51:40 -0400
Subject: [PATCH 15/74] Fix log severity mapping for guide sample (#2249)
---
docs/guides/other_libs/samples/ModifyLogMethod.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/guides/other_libs/samples/ModifyLogMethod.cs b/docs/guides/other_libs/samples/ModifyLogMethod.cs
index b4870cfd1..0f7c11daf 100644
--- a/docs/guides/other_libs/samples/ModifyLogMethod.cs
+++ b/docs/guides/other_libs/samples/ModifyLogMethod.cs
@@ -6,8 +6,8 @@ private static async Task LogAsync(LogMessage message)
LogSeverity.Error => LogEventLevel.Error,
LogSeverity.Warning => LogEventLevel.Warning,
LogSeverity.Info => LogEventLevel.Information,
- LogSeverity.Verbose => LogEventLevel.Debug,
- LogSeverity.Debug => LogEventLevel.Verbose,
+ LogSeverity.Verbose => LogEventLevel.Verbose,
+ LogSeverity.Debug => LogEventLevel.Debug,
_ => LogEventLevel.Information
};
Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message);
From daba58cdd4ec699617f35320072ab95e8c04c317 Mon Sep 17 00:00:00 2001
From: Alex Thomson
Date: Mon, 18 Apr 2022 18:52:32 +1200
Subject: [PATCH 16/74] Fix SocketGuild not returning the AudioClient (#2248)
---
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
index 49d2cd3bd..8b376b3ed 100644
--- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
+++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
@@ -1781,7 +1781,7 @@ namespace Discord.WebSocket
///
ulong? IGuild.AFKChannelId => AFKChannelId;
///
- IAudioClient IGuild.AudioClient => null;
+ IAudioClient IGuild.AudioClient => AudioClient;
///
bool IGuild.Available => true;
///
From 42c65bc879c04b528446987bf11c2cd54188a573 Mon Sep 17 00:00:00 2001
From: Denis Voitenko
Date: Mon, 18 Apr 2022 09:56:32 +0300
Subject: [PATCH 17/74] Typo in comment (#2242)
---
samples/InteractionFramework/Modules/ExampleModule.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/samples/InteractionFramework/Modules/ExampleModule.cs b/samples/InteractionFramework/Modules/ExampleModule.cs
index 1c0a6c8a2..21064bbe3 100644
--- a/samples/InteractionFramework/Modules/ExampleModule.cs
+++ b/samples/InteractionFramework/Modules/ExampleModule.cs
@@ -14,7 +14,7 @@ namespace InteractionFramework.Modules
private InteractionHandler _handler;
- // Constructor injection is also a valid way to access the dependecies
+ // Constructor injection is also a valid way to access the dependencies
public ExampleModule(InteractionHandler handler)
{
_handler = handler;
From e1a8ecd723ec3c73706cbca88d606b4bd8d8825d Mon Sep 17 00:00:00 2001
From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com>
Date: Mon, 18 Apr 2022 04:00:58 -0300
Subject: [PATCH 18/74] [Robot] Add missing json error (#2237)
* Add 10087, 30047, 30048, 40061 Error codes
* Update src/Discord.Net.Core/DiscordErrorCode.cs
* Update src/Discord.Net.Core/DiscordErrorCode.cs
Co-authored-by: Discord.Net Robot
Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>
---
src/Discord.Net.Core/DiscordErrorCode.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs
index e9ed63e58..51fd736f6 100644
--- a/src/Discord.Net.Core/DiscordErrorCode.cs
+++ b/src/Discord.Net.Core/DiscordErrorCode.cs
@@ -58,6 +58,7 @@ namespace Discord
#endregion
#region General Actions (20XXX)
+ UnknownTag = 10087,
BotsCannotUse = 20001,
OnlyBotsCanUse = 20002,
CannotSendExplicitContent = 20009,
@@ -98,6 +99,8 @@ namespace Discord
#region General Request Errors (40XXX)
MaximumNumberOfEditsReached = 30046,
+ MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047,
+ MaximumNumberOfTagsInAForumChannelReached = 30048,
TokenUnauthorized = 40001,
InvalidVerification = 40002,
OpeningDMTooFast = 40003,
@@ -112,6 +115,7 @@ namespace Discord
#region Action Preconditions/Checks (50XXX)
InteractionHasAlreadyBeenAcknowledged = 40060,
+ TagNamesMustBeUnique = 40061,
MissingPermissions = 50001,
InvalidAccountType = 50002,
CannotExecuteForDM = 50003,
From 18f001e37b5bbdd37b51f933b3b9c89d38c0c400 Mon Sep 17 00:00:00 2001
From: Misha133 <61027276+Misha-133@users.noreply.github.com>
Date: Fri, 22 Apr 2022 13:26:54 +0300
Subject: [PATCH 19/74] [DOCS] Group commands example (#2246)
* add Group Command Examples to int_framework intro
* update subcommad group's name
* added some comments t othe example code
* fixed naming
* added spaces in comments
---
docs/guides/int_framework/intro.md | 2 ++
.../samples/intro/groupmodule.cs | 21 +++++++++++++++++++
2 files changed, 23 insertions(+)
create mode 100644 docs/guides/int_framework/samples/intro/groupmodule.cs
diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md
index abea2a735..f9eca370a 100644
--- a/docs/guides/int_framework/intro.md
+++ b/docs/guides/int_framework/intro.md
@@ -282,6 +282,8 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can
> Although creating nested module stuctures are allowed,
> you are not permitted to use more than 2 [GroupAttribute]'s in module hierarchy.
+[!code-csharp[Command Group Example](samples/intro/groupmodule.cs)]
+
## Executing Commands
Any of the following socket events can be used to execute commands:
diff --git a/docs/guides/int_framework/samples/intro/groupmodule.cs b/docs/guides/int_framework/samples/intro/groupmodule.cs
new file mode 100644
index 000000000..f0d992aff
--- /dev/null
+++ b/docs/guides/int_framework/samples/intro/groupmodule.cs
@@ -0,0 +1,21 @@
+// You can put commands in groups
+[Group("group-name", "Group description")]
+public class CommandGroupModule : InteractionModuleBase
+{
+ // This command will look like
+ // group-name ping
+ [SlashCommand("ping", "Get a pong")]
+ public async Task PongSubcommand()
+ => await RespondAsync("Pong!");
+
+ // And even in sub-command groups
+ [Group("subcommand-group-name", "Subcommand group description")]
+ public class SubСommandGroupModule : InteractionModuleBase
+ {
+ // This command will look like
+ // group-name subcommand-group-name echo
+ [SlashCommand("echo", "Echo an input")]
+ public async Task EchoSubcommand(string input)
+ => await RespondAsync(input);
+ }
+}
\ No newline at end of file
From f2d383c955e4f3a93ea4f5b8aaf954c64ea8c195 Mon Sep 17 00:00:00 2001
From: Quin Lynch <49576606+quinchs@users.noreply.github.com>
Date: Wed, 27 Apr 2022 10:59:50 -0300
Subject: [PATCH 20/74] remove extra header from readme
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 541948f4b..bb8437432 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-# Discord.Net
From 275b833205e29244106640af61e9df26d7973d39 Mon Sep 17 00:00:00 2001
From: Alex Thomson
Date: Thu, 28 Apr 2022 02:07:35 +1200
Subject: [PATCH 21/74] Fix browser property (#2254)
---
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
index 21594fed7..cca2de203 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
@@ -274,7 +274,7 @@ namespace Discord.API
{
["$device"] = "Discord.Net",
["$os"] = Environment.OSVersion.Platform.ToString(),
- [$"browser"] = "Discord.Net"
+ ["$browser"] = "Discord.Net"
};
var msg = new IdentifyParams()
{
From 26c1a7e80f4e2d73e607fa87708e52724bbe7349 Mon Sep 17 00:00:00 2001
From: Diego-VP20 <69905156+Diego-VP20@users.noreply.github.com>
Date: Wed, 27 Apr 2022 16:08:07 +0200
Subject: [PATCH 22/74] docs: Add files to the parameters (#2244)
---
.../application-commands/slash-commands/parameters.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/docs/guides/int_basics/application-commands/slash-commands/parameters.md b/docs/guides/int_basics/application-commands/slash-commands/parameters.md
index 6afd83729..4f3cd2e8c 100644
--- a/docs/guides/int_basics/application-commands/slash-commands/parameters.md
+++ b/docs/guides/int_basics/application-commands/slash-commands/parameters.md
@@ -15,9 +15,10 @@ Slash commands can have a bunch of parameters, each their own type. Let's first
| Integer | A number. |
| Boolean | True or False. |
| User | A user |
-| Channel | A channel, this includes voice text and categories |
| Role | A role. |
+| Channel | A channel, this includes voice text and categories |
| Mentionable | A role or a user. |
+| File | A file |
Each one of the parameter types has its own DNET type in the `SocketSlashCommandDataOption`'s Value field:
| Name | C# Type |
@@ -31,6 +32,7 @@ Each one of the parameter types has its own DNET type in the `SocketSlashCommand
| Role | `SocketRole` |
| Channel | `SocketChannel` |
| Mentionable | `SocketUser`, `SocketGuildUser`, or `SocketRole` |
+| File | `IAttachment` |
Let's start by making a command that takes in a user and lists their roles.
From 4ce1801bdf56b8b7bc0eca7a5c1e4353ab208d64 Mon Sep 17 00:00:00 2001
From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Date: Wed, 27 Apr 2022 17:09:30 +0300
Subject: [PATCH 23/74] feature: Passing CustomId matches into contexts (#2136)
* add logic for passing the wild card captures into the context
* move concrete impl of IRouteSegmentMatch to internal
* Apply suggestions from code review
Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>
* fix build errors
* Apply suggestions from code review
Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>
Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>
Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com>
---
.../Interactions/IRouteMatchContainer.cs | 24 +++++++++++++++++++
.../Interactions/IRouteSegmentMatch.cs | 16 +++++++++++++
.../Interactions/RouteSegmentMatch.cs | 16 +++++++++++++
.../InteractionContext.cs | 14 ++++++++++-
.../InteractionService.cs | 19 +++++++++++++++
.../Interactions/RestInteractionContext.cs | 14 ++++++++++-
.../Interactions/SocketInteractionContext.cs | 14 ++++++++++-
7 files changed, 114 insertions(+), 3 deletions(-)
create mode 100644 src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs
create mode 100644 src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs
create mode 100644 src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs
diff --git a/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs b/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs
new file mode 100644
index 000000000..f9a3a3183
--- /dev/null
+++ b/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace Discord
+{
+ ///
+ /// Represents a container for temporarily storing CustomId wild card matches of a component.
+ ///
+ public interface IRouteMatchContainer
+ {
+ ///
+ /// Gets the collection of captured route segments in this container.
+ ///
+ ///
+ /// A collection of captured route segments.
+ ///
+ IEnumerable SegmentMatches { get; }
+
+ ///
+ /// Sets the property of this container.
+ ///
+ /// The collection of captured route segments.
+ void SetSegmentMatches(IEnumerable segmentMatches);
+ }
+}
diff --git a/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs b/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs
new file mode 100644
index 000000000..675bd6754
--- /dev/null
+++ b/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs
@@ -0,0 +1,16 @@
+namespace Discord
+{
+ ///
+ /// Represents an object for storing a CustomId wild card match.
+ ///
+ public interface IRouteSegmentMatch
+ {
+ ///
+ /// Gets the captured value of this wild card match.
+ ///
+ ///
+ /// The value of this wild card.
+ ///
+ string Value { get; }
+ }
+}
diff --git a/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs b/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs
new file mode 100644
index 000000000..f1d80cfea
--- /dev/null
+++ b/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs
@@ -0,0 +1,16 @@
+namespace Discord
+{
+ ///
+ /// Represents an object for storing a CustomId wild card match.
+ ///
+ internal record RouteSegmentMatch : IRouteSegmentMatch
+ {
+ ///
+ public string Value { get; }
+
+ public RouteSegmentMatch(string value)
+ {
+ Value = value;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/InteractionContext.cs b/src/Discord.Net.Interactions/InteractionContext.cs
index 99a8d8736..024ab5ef8 100644
--- a/src/Discord.Net.Interactions/InteractionContext.cs
+++ b/src/Discord.Net.Interactions/InteractionContext.cs
@@ -1,7 +1,10 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+
namespace Discord.Interactions
{
///
- public class InteractionContext : IInteractionContext
+ public class InteractionContext : IInteractionContext, IRouteMatchContainer
{
///
public IDiscordClient Client { get; }
@@ -13,6 +16,8 @@ namespace Discord.Interactions
public IUser User { get; }
///
public IDiscordInteraction Interaction { get; }
+ ///
+ public IReadOnlyCollection SegmentMatches { get; private set; }
///
/// Initializes a new .
@@ -30,5 +35,12 @@ namespace Discord.Interactions
User = interaction.User;
Interaction = interaction;
}
+
+ ///
+ public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray();
+
+ //IRouteMatchContainer
+ ///
+ IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches;
}
}
diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs
index 01fb8cc9d..8eb5799d6 100644
--- a/src/Discord.Net.Interactions/InteractionService.cs
+++ b/src/Discord.Net.Interactions/InteractionService.cs
@@ -775,6 +775,9 @@ namespace Discord.Interactions
await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}
+
+ SetMatchesIfApplicable(context, result);
+
return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}
@@ -819,9 +822,25 @@ namespace Discord.Interactions
await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false);
return result;
}
+
+ SetMatchesIfApplicable(context, result);
+
return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
}
+ private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult)
+ where T : class, ICommandInfo
+ {
+ if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer)
+ return;
+
+ var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length];
+ for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++)
+ matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]);
+
+ matchContainer.SetSegmentMatches(matches);
+ }
+
internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null)
=> _typeConverterMap.Get(type, services);
diff --git a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs
index 196c6133b..d407f5103 100644
--- a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs
+++ b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Threading.Tasks;
namespace Discord.Rest
@@ -6,7 +8,7 @@ namespace Discord.Rest
///
/// Represents a Rest based context of an .
///
- public class RestInteractionContext : IRestInteractionContext
+ public class RestInteractionContext : IRestInteractionContext, IRouteMatchContainer
where TInteraction : RestInteraction
{
///
@@ -45,6 +47,9 @@ namespace Discord.Rest
///
public Func InteractionResponseCallback { get; set; }
+ ///
+ public IReadOnlyCollection SegmentMatches { get; private set; }
+
///
/// Initializes a new .
///
@@ -71,6 +76,13 @@ namespace Discord.Rest
InteractionResponseCallback = interactionResponseCallback;
}
+ ///
+ public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray();
+
+ //IRouteMatchContainer
+ ///
+ IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches;
+
// IInterationContext
///
IDiscordClient IInteractionContext.Client => Client;
diff --git a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs
index 4cd9ef264..a2a101839 100644
--- a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs
+++ b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs
@@ -1,11 +1,13 @@
using Discord.WebSocket;
+using System.Collections.Generic;
+using System.Collections.Immutable;
namespace Discord.Interactions
{
///
/// Represents a Web-Socket based context of an .
///
- public class SocketInteractionContext : IInteractionContext
+ public class SocketInteractionContext : IInteractionContext, IRouteMatchContainer
where TInteraction : SocketInteraction
{
///
@@ -36,6 +38,9 @@ namespace Discord.Interactions
///
public TInteraction Interaction { get; }
+ ///
+ public IReadOnlyCollection SegmentMatches { get; private set; }
+
///
/// Initializes a new .
///
@@ -50,6 +55,13 @@ namespace Discord.Interactions
Interaction = interaction;
}
+ ///
+ public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray();
+
+ //IRouteMatchContainer
+ ///
+ IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches;
+
// IInteractionContext
///
IDiscordClient IInteractionContext.Client => Client;
From d98b3cc495e9230346e38fbd3e7ff4f95c332ef1 Mon Sep 17 00:00:00 2001
From: Quin Lynch <49576606+quinchs@users.noreply.github.com>
Date: Thu, 28 Apr 2022 08:47:52 -0300
Subject: [PATCH 24/74] feature: V2 Permissions (#2222)
* Initial V2 permissions
* add perms-v2 attributes and properties, add deprecation messages
* add perms-v2 properties to command info classes
* add perms-v2 fields to Rest/SocketApplicationCommand entities and IApplicationCommand
* fix json name of DmPermission field
Co-authored-by: Cenngo
---
.../ApplicationCommandProperties.cs | 10 +++++
.../ContextMenus/MessageCommandBuilder.cs | 36 +++++++++++++++++-
.../ContextMenus/UserCommandBuilder.cs | 36 +++++++++++++++++-
.../Interactions/IApplicationCommand.cs | 13 +++++++
.../SlashCommands/SlashCommandBuilder.cs | 34 +++++++++++++++++
.../DefaultMemberPermissionAttribute.cs | 25 ++++++++++++
.../Attributes/DefaultPermissionAttribute.cs | 1 +
.../Attributes/EnabledInDmAttribute.cs | 25 ++++++++++++
.../Commands/ContextCommandBuilder.cs | 38 +++++++++++++++++++
.../Builders/Commands/SlashCommandBuilder.cs | 38 +++++++++++++++++++
.../Builders/ModuleBuilder.cs | 38 +++++++++++++++++++
.../Builders/ModuleClassBuilder.cs | 30 +++++++++++++++
.../ContextCommands/ContextCommandInfo.cs | 8 ++++
.../Info/Commands/SlashCommandInfo.cs | 8 ++++
.../Info/IApplicationCommandInfo.cs | 13 +++++++
.../Info/ModuleInfo.cs | 28 ++++++++++++++
.../Utilities/ApplicationCommandRestUtil.cs | 21 ++++++++--
.../API/Common/ApplicationCommand.cs | 7 ++++
.../Rest/CreateApplicationCommandParams.cs | 6 +++
.../Interactions/InteractionHelper.cs | 25 ++++++++++--
.../Interactions/RestApplicationCommand.cs | 10 +++++
.../SocketApplicationCommand.cs | 10 +++++
22 files changed, 451 insertions(+), 9 deletions(-)
create mode 100644 src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs
create mode 100644 src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs
diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
index 501a0e905..9b3ac8453 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
@@ -17,6 +17,16 @@ namespace Discord
///
public Optional IsDefaultPermission { get; set; }
+ ///
+ /// Gets or sets whether or not this command can be used in DMs.
+ ///
+ public Optional IsDMEnabled { get; set; }
+
+ ///
+ /// Gets or sets the default permissions required by a user to execute this application command.
+ ///
+ public Optional DefaultMemberPermissions { get; set; }
+
internal ApplicationCommandProperties() { }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
index c7a7cf741..59040dd4e 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
@@ -31,6 +31,16 @@ namespace Discord
///
public bool IsDefaultPermission { get; set; } = true;
+ ///
+ /// Gets or sets whether or not this command can be used in DMs.
+ ///
+ public bool IsDMEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the default permission required to use this slash command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; set; }
+
private string _name;
///
@@ -44,7 +54,9 @@ namespace Discord
var props = new MessageCommandProperties
{
Name = Name,
- IsDefaultPermission = IsDefaultPermission
+ IsDefaultPermission = IsDefaultPermission,
+ IsDMEnabled = IsDMEnabled,
+ DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified
};
return props;
@@ -73,5 +85,27 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}
+
+ ///
+ /// Sets whether or not this command can be used in dms
+ ///
+ /// if the command is available in dms, otherwise .
+ /// The current builder.
+ public MessageCommandBuilder WithDMPermission(bool permission)
+ {
+ IsDMEnabled = permission;
+ return this;
+ }
+
+ ///
+ /// Sets the default member permissions required to use this application command.
+ ///
+ /// The permissions required to use this command.
+ /// The current builder.
+ public MessageCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
+ {
+ DefaultMemberPermissions = permissions;
+ return this;
+ }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
index bd1078be3..7c82dce55 100644
--- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
@@ -31,6 +31,16 @@ namespace Discord
///
public bool IsDefaultPermission { get; set; } = true;
+ ///
+ /// Gets or sets whether or not this command can be used in DMs.
+ ///
+ public bool IsDMEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the default permission required to use this slash command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; set; }
+
private string _name;
///
@@ -42,7 +52,9 @@ namespace Discord
var props = new UserCommandProperties
{
Name = Name,
- IsDefaultPermission = IsDefaultPermission
+ IsDefaultPermission = IsDefaultPermission,
+ IsDMEnabled = IsDMEnabled,
+ DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified
};
return props;
@@ -71,5 +83,27 @@ namespace Discord
IsDefaultPermission = isDefaultPermission;
return this;
}
+
+ ///
+ /// Sets whether or not this command can be used in dms
+ ///
+ /// if the command is available in dms, otherwise .
+ /// The current builder.
+ public UserCommandBuilder WithDMPermission(bool permission)
+ {
+ IsDMEnabled = permission;
+ return this;
+ }
+
+ ///
+ /// Sets the default member permissions required to use this application command.
+ ///
+ /// The permissions required to use this command.
+ /// The current builder.
+ public UserCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
+ {
+ DefaultMemberPermissions = permissions;
+ return this;
+ }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
index 72045a52a..58a002649 100644
--- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
@@ -34,6 +34,19 @@ namespace Discord
///
bool IsDefaultPermission { get; }
+ ///
+ /// Indicates whether the command is available in DMs with the app.
+ ///
+ ///
+ /// Only for globally-scoped commands.
+ ///
+ bool IsEnabledInDm { get; }
+
+ ///
+ /// Set of default required to invoke the command.
+ ///
+ GuildPermissions DefaultMemberPermissions { get; }
+
///
/// Gets a collection of options for this application command.
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
index ccfb2da0a..ed815ca1a 100644
--- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
@@ -81,6 +81,16 @@ namespace Discord
///
public bool IsDefaultPermission { get; set; } = true;
+ ///
+ /// Gets or sets whether or not this command can be used in DMs.
+ ///
+ public bool IsDMEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the default permission required to use this slash command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; set; }
+
private string _name;
private string _description;
private List _options;
@@ -96,6 +106,8 @@ namespace Discord
Name = Name,
Description = Description,
IsDefaultPermission = IsDefaultPermission,
+ IsDMEnabled = IsDMEnabled,
+ DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified
};
if (Options != null && Options.Any())
@@ -145,6 +157,28 @@ namespace Discord
return this;
}
+ ///
+ /// Sets whether or not this command can be used in dms
+ ///
+ /// if the command is available in dms, otherwise .
+ /// The current builder.
+ public SlashCommandBuilder WithDMPermission(bool permission)
+ {
+ IsDMEnabled = permission;
+ return this;
+ }
+
+ ///
+ /// Sets the default member permissions required to use this application command.
+ ///
+ /// The permissions required to use this command.
+ /// The current builder.
+ public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions)
+ {
+ DefaultMemberPermissions = permissions;
+ return this;
+ }
+
///
/// Adds an option to the current slash command.
///
diff --git a/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs
new file mode 100644
index 000000000..ec79da1e3
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Discord.Interactions
+{
+ ///
+ /// Sets the of an application command or module.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class DefaultMemberPermissionsAttribute : Attribute
+ {
+ ///
+ /// Gets the default permission required to use this command.
+ ///
+ public GuildPermission Permissions { get; }
+
+ ///
+ /// Sets the of an application command or module.
+ ///
+ /// The default permission required to use this command.
+ public DefaultMemberPermissionsAttribute(GuildPermission permissions)
+ {
+ Permissions = permissions;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs
index ed0a532be..2e03dfac6 100644
--- a/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs
@@ -6,6 +6,7 @@ namespace Discord.Interactions
/// Set the "Default Permission" property of an Application Command.
///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ [Obsolete($"Soon to be deprecated, use Permissions-v2 attributes like {nameof(EnabledInDmAttribute)} and {nameof(DefaultMemberPermissionsAttribute)}")]
public class DefaultPermissionAttribute : Attribute
{
///
diff --git a/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs b/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs
new file mode 100644
index 000000000..a97f85a25
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Discord.Interactions
+{
+ ///
+ /// Sets the property of an application command or module.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class EnabledInDmAttribute : Attribute
+ {
+ ///
+ /// Gets whether or not this command can be used in DMs.
+ ///
+ public bool IsEnabled { get; }
+
+ ///
+ /// Sets the property of an application command or module.
+ ///
+ /// Whether or not this command can be used in DMs.
+ public EnabledInDmAttribute(bool isEnabled)
+ {
+ IsEnabled = isEnabled;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs
index d40547b3c..be0e5eb70 100644
--- a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs
@@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders
///
/// Gets the default permission of this command.
///
+ [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; set; } = true;
+ ///
+ /// Gets whether this command can be used in DMs.
+ ///
+ public bool IsEnabledInDm { get; set; } = true;
+
+ ///
+ /// Gets the default permissions needed for executing this command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; set; } = null;
+
internal ContextCommandBuilder (ModuleBuilder module) : base(module) { }
///
@@ -49,6 +60,7 @@ namespace Discord.Interactions.Builders
///
/// The builder instance.
///
+ [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")]
public ContextCommandBuilder SetDefaultPermission (bool defaultPermision)
{
DefaultPermission = defaultPermision;
@@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders
return this;
}
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public ContextCommandBuilder SetEnabledInDm(bool isEnabled)
+ {
+ IsEnabledInDm = isEnabled;
+ return this;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public ContextCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions)
+ {
+ DefaultMemberPermissions = permissions;
+ return this;
+ }
+
internal override ContextCommandInfo Build (ModuleInfo module, InteractionService commandService) =>
ContextCommandInfo.Create(this, module, commandService);
}
diff --git a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
index d8e9b0658..cd9bdfc24 100644
--- a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
@@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders
///
/// Gets and sets the default permission of this command.
///
+ [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; set; } = true;
+ ///
+ /// Gets whether this command can be used in DMs.
+ ///
+ public bool IsEnabledInDm { get; set; } = true;
+
+ ///
+ /// Gets the default permissions needed for executing this command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; set; } = null;
+
internal SlashCommandBuilder (ModuleBuilder module) : base(module) { }
///
@@ -49,6 +60,7 @@ namespace Discord.Interactions.Builders
///
/// The builder instance.
///
+ [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")]
public SlashCommandBuilder WithDefaultPermission (bool permission)
{
DefaultPermission = permission;
@@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders
return this;
}
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public SlashCommandBuilder SetEnabledInDm(bool isEnabled)
+ {
+ IsEnabledInDm = isEnabled;
+ return this;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions)
+ {
+ DefaultMemberPermissions = permissions;
+ return this;
+ }
+
internal override SlashCommandInfo Build (ModuleInfo module, InteractionService commandService) =>
new SlashCommandInfo(this, module, commandService);
}
diff --git a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs
index 40c263643..b7f00025f 100644
--- a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs
@@ -51,8 +51,19 @@ namespace Discord.Interactions.Builders
///
/// Gets and sets the default permission of this module.
///
+ [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; set; } = true;
+ ///
+ /// Gets whether this command can be used in DMs.
+ ///
+ public bool IsEnabledInDm { get; set; } = true;
+
+ ///
+ /// Gets the default permissions needed for executing this command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; set; } = null;
+
///
/// Gets and sets whether this has a .
///
@@ -159,12 +170,39 @@ namespace Discord.Interactions.Builders
///
/// The builder instance.
///
+ [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")]
public ModuleBuilder WithDefaultPermission (bool permission)
{
DefaultPermission = permission;
return this;
}
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public ModuleBuilder SetEnabledInDm(bool isEnabled)
+ {
+ IsEnabledInDm = isEnabled;
+ return this;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public ModuleBuilder WithDefaultMemberPermissions(GuildPermission permissions)
+ {
+ DefaultMemberPermissions = permissions;
+ return this;
+ }
+
///
/// Adds attributes to .
///
diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
index b2317d1f3..1bbdfcc4a 100644
--- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
@@ -85,6 +85,16 @@ namespace Discord.Interactions.Builders
builder.DefaultPermission = defPermission.IsDefaultPermission;
}
break;
+ case EnabledInDmAttribute enabledInDm:
+ {
+ builder.IsEnabledInDm = enabledInDm.IsEnabled;
+ }
+ break;
+ case DefaultMemberPermissionsAttribute memberPermission:
+ {
+ builder.DefaultMemberPermissions = memberPermission.Permissions;
+ }
+ break;
case PreconditionAttribute precondition:
builder.AddPreconditions(precondition);
break;
@@ -169,6 +179,16 @@ namespace Discord.Interactions.Builders
builder.DefaultPermission = defaultPermission.IsDefaultPermission;
}
break;
+ case EnabledInDmAttribute enabledInDm:
+ {
+ builder.IsEnabledInDm = enabledInDm.IsEnabled;
+ }
+ break;
+ case DefaultMemberPermissionsAttribute memberPermission:
+ {
+ builder.DefaultMemberPermissions = memberPermission.Permissions;
+ }
+ break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
@@ -211,6 +231,16 @@ namespace Discord.Interactions.Builders
builder.DefaultPermission = defaultPermission.IsDefaultPermission;
}
break;
+ case EnabledInDmAttribute enabledInDm:
+ {
+ builder.IsEnabledInDm = enabledInDm.IsEnabled;
+ }
+ break;
+ case DefaultMemberPermissionsAttribute memberPermission:
+ {
+ builder.DefaultMemberPermissions = memberPermission.Permissions;
+ }
+ break;
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
diff --git a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs
index 4c2e7af7d..2d6d748d4 100644
--- a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs
@@ -17,6 +17,12 @@ namespace Discord.Interactions
///
public bool DefaultPermission { get; }
+ ///
+ public bool IsEnabledInDm { get; }
+
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; }
+
///
public override IReadOnlyCollection Parameters { get; }
@@ -31,6 +37,8 @@ namespace Discord.Interactions
{
CommandType = builder.CommandType;
DefaultPermission = builder.DefaultPermission;
+ IsEnabledInDm = builder.IsEnabledInDm;
+ DefaultMemberPermissions = builder.DefaultMemberPermissions;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
}
diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
index a123ac183..e428144c7 100644
--- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
@@ -26,6 +26,12 @@ namespace Discord.Interactions
///
public bool DefaultPermission { get; }
+ ///
+ public bool IsEnabledInDm { get; }
+
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; }
+
///
public override IReadOnlyCollection Parameters { get; }
@@ -41,6 +47,8 @@ namespace Discord.Interactions
{
Description = builder.Description;
DefaultPermission = builder.DefaultPermission;
+ IsEnabledInDm = builder.IsEnabledInDm;
+ DefaultMemberPermissions = builder.DefaultMemberPermissions;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray();
diff --git a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs
index 1e0d532b0..dd1b97899 100644
--- a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs
+++ b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace Discord.Interactions
{
///
@@ -18,6 +20,17 @@ namespace Discord.Interactions
///
/// Gets the DefaultPermission of this command.
///
+ [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
bool DefaultPermission { get; }
+
+ ///
+ /// Gets whether this command can be used in DMs.
+ ///
+ public bool IsEnabledInDm { get; }
+
+ ///
+ /// Gets the default permissions needed for executing this command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; }
}
}
diff --git a/src/Discord.Net.Interactions/Info/ModuleInfo.cs b/src/Discord.Net.Interactions/Info/ModuleInfo.cs
index 321e0bfa9..904d67410 100644
--- a/src/Discord.Net.Interactions/Info/ModuleInfo.cs
+++ b/src/Discord.Net.Interactions/Info/ModuleInfo.cs
@@ -41,8 +41,19 @@ namespace Discord.Interactions
///
/// Gets the default Permission of this module.
///
+ [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")]
public bool DefaultPermission { get; }
+ ///
+ /// Gets whether this command can be used in DMs.
+ ///
+ public bool IsEnabledInDm { get; }
+
+ ///
+ /// Gets the default permissions needed for executing this command.
+ ///
+ public GuildPermission? DefaultMemberPermissions { get; }
+
///
/// Gets the collection of Sub Modules of this module.
///
@@ -110,6 +121,8 @@ namespace Discord.Interactions
Description = builder.Description;
Parent = parent;
DefaultPermission = builder.DefaultPermission;
+ IsEnabledInDm = builder.IsEnabledInDm;
+ DefaultMemberPermissions = BuildDefaultMemberPermissions(builder);
SlashCommands = BuildSlashCommands(builder).ToImmutableArray();
ContextCommands = BuildContextCommands(builder).ToImmutableArray();
ComponentCommands = BuildComponentCommands(builder).ToImmutableArray();
@@ -226,5 +239,20 @@ namespace Discord.Interactions
}
return true;
}
+
+ private static GuildPermission? BuildDefaultMemberPermissions(ModuleBuilder builder)
+ {
+ var permissions = builder.DefaultMemberPermissions;
+
+ var parent = builder.Parent;
+
+ while (parent != null)
+ {
+ permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0);
+ parent = parent.Parent;
+ }
+
+ return permissions;
+ }
}
}
diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
index c2052b7c7..60980c065 100644
--- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
+++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
@@ -40,7 +40,8 @@ namespace Discord.Interactions
{
Name = commandInfo.Name,
Description = commandInfo.Description,
- IsDefaultPermission = commandInfo.DefaultPermission,
+ IsDMEnabled = commandInfo.IsEnabledInDm,
+ DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)
}.Build();
if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount)
@@ -64,8 +65,20 @@ namespace Discord.Interactions
public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo)
=> commandInfo.CommandType switch
{
- ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}.Build(),
- ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}.Build(),
+ ApplicationCommandType.Message => new MessageCommandBuilder
+ {
+ Name = commandInfo.Name,
+ IsDefaultPermission = commandInfo.DefaultPermission,
+ DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0),
+ IsDMEnabled = commandInfo.IsEnabledInDm
+ }.Build(),
+ ApplicationCommandType.User => new UserCommandBuilder
+ {
+ Name = commandInfo.Name,
+ IsDefaultPermission = commandInfo.DefaultPermission,
+ DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0),
+ IsDMEnabled = commandInfo.IsEnabledInDm
+ }.Build(),
_ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.")
};
#endregion
@@ -113,6 +126,8 @@ namespace Discord.Interactions
Name = moduleInfo.SlashGroupName,
Description = moduleInfo.Description,
IsDefaultPermission = moduleInfo.DefaultPermission,
+ IsDMEnabled = moduleInfo.IsEnabledInDm,
+ DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions
}.Build();
if (options.Count > SlashCommandBuilder.MaxOptionsCount)
diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
index 81598b96e..8b84149dd 100644
--- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
+++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
@@ -24,5 +24,12 @@ namespace Discord.API
[JsonProperty("default_permission")]
public Optional DefaultPermissions { get; set; }
+
+ // V2 Permissions
+ [JsonProperty("dm_permission")]
+ public Optional DmPermission { get; set; }
+
+ [JsonProperty("default_member_permissions")]
+ public Optional DefaultMemberPermission { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
index 82f0befcd..7ae8718b6 100644
--- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
+++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
@@ -19,6 +19,12 @@ namespace Discord.API.Rest
[JsonProperty("default_permission")]
public Optional DefaultPermission { get; set; }
+ [JsonProperty("dm_permission")]
+ public Optional DmPermission { get; set; }
+
+ [JsonProperty("default_member_permissions")]
+ public Optional DefaultMemberPermission { get; set; }
+
public CreateApplicationCommandParams() { }
public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null)
{
diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
index e345bfa94..74d7953ad 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
@@ -100,7 +100,12 @@ namespace Discord.Rest
Type = arg.Type,
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
- : Optional.Unspecified
+ : Optional.Unspecified,
+
+ // TODO: better conversion to nullable optionals
+ DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
+ DmPermission = arg.IsDMEnabled.ToNullable()
+
};
if (arg is SlashCommandProperties slashProps)
@@ -134,7 +139,11 @@ namespace Discord.Rest
Type = arg.Type,
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
- : Optional.Unspecified
+ : Optional.Unspecified,
+
+ // TODO: better conversion to nullable optionals
+ DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
+ DmPermission = arg.IsDMEnabled.ToNullable()
};
if (arg is SlashCommandProperties slashProps)
@@ -171,7 +180,11 @@ namespace Discord.Rest
Type = arg.Type,
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
- : Optional.Unspecified
+ : Optional.Unspecified,
+
+ // TODO: better conversion to nullable optionals
+ DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
+ DmPermission = arg.IsDMEnabled.ToNullable()
};
if (arg is SlashCommandProperties slashProps)
@@ -285,7 +298,11 @@ namespace Discord.Rest
Type = arg.Type,
DefaultPermission = arg.IsDefaultPermission.IsSpecified
? arg.IsDefaultPermission.Value
- : Optional.Unspecified
+ : Optional.Unspecified,
+
+ // TODO: better conversion to nullable optionals
+ DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(),
+ DmPermission = arg.IsDMEnabled.ToNullable()
};
if (arg is SlashCommandProperties slashProps)
diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
index ea8d5bc42..9e2bab2c2 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
@@ -27,6 +27,12 @@ namespace Discord.Rest
///
public bool IsDefaultPermission { get; private set; }
+ ///
+ public bool IsEnabledInDm { get; private set; }
+
+ ///
+ public GuildPermissions DefaultMemberPermissions { get; private set; }
+
///
/// Gets a collection of options for this command.
///
@@ -57,6 +63,10 @@ namespace Discord.Rest
Options = model.Options.IsSpecified
? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray()
: ImmutableArray.Create();
+
+ IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true);
+ DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified
+ ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None;
}
///
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
index 36eba0cd1..40ec17f5b 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs
@@ -36,6 +36,12 @@ namespace Discord.WebSocket
///
public bool IsDefaultPermission { get; private set; }
+ ///
+ public bool IsEnabledInDm { get; private set; }
+
+ ///
+ public GuildPermissions DefaultMemberPermissions { get; private set; }
+
///
/// Gets a collection of s for this command.
///
@@ -86,6 +92,10 @@ namespace Discord.WebSocket
Options = model.Options.IsSpecified
? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray()
: ImmutableArray.Create();
+
+ IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true);
+ DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified
+ ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None;
}
///
From 2b49322a54d252c80b91756325f54f42ced80016 Mon Sep 17 00:00:00 2001
From: Ge
Date: Thu, 28 Apr 2022 19:48:11 +0800
Subject: [PATCH 25/74] docs: Fix TextCommands reference in first-bot.md
(#2264)
---
docs/guides/getting_started/first-bot.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md
index e1af20d30..a5b0dbbd4 100644
--- a/docs/guides/getting_started/first-bot.md
+++ b/docs/guides/getting_started/first-bot.md
@@ -202,7 +202,7 @@ online in Discord.
To create commands for your bot, you may choose from a variety of
command processors available. Throughout the guides, we will be using
-the one that Discord.Net ships with. @Guides.Commands.Intro will
+the one that Discord.Net ships with. @Guides.TextCommands.Intro will
guide you through how to setup a program that is ready for
[CommandService].
From f5dbb95610d7a5cff5f33c2075c316b05e6ae5ed Mon Sep 17 00:00:00 2001
From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Date: Thu, 28 Apr 2022 14:48:37 +0300
Subject: [PATCH 26/74] docs: Interaction Service Perms-v2 docs (#2263)
* add perms v2 docs
* add perms v2 docs
---
docs/guides/int_framework/intro.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md
index f9eca370a..c019b1424 100644
--- a/docs/guides/int_framework/intro.md
+++ b/docs/guides/int_framework/intro.md
@@ -158,6 +158,14 @@ Interaction service complex parameter constructors are prioritized in the follow
2. Constuctor tagged with `[ComplexParameterCtor]`.
3. Type's only public constuctor.
+#### DM Permissions
+
+You can use the [EnabledInDmAttribute] to configure whether a globally-scoped top level command should be enabled in Dms or not. Only works on top level commands.
+
+#### Default Member Permissions
+
+[DefaultMemberPermissionsAttribute] can be used when creating a command to set the permissions a user must have to use the command. Permission overwrites can be configured from the Integrations page of Guild Settings. [DefaultMemberPermissionsAttribute] cumulatively propagates down the class hierarchy until it reaches a top level command. This attribute can be only used on top level commands and will not work on commands that are nested in command groups.
+
## User Commands
A valid User Command must have the following structure:
From 0554ac24429c7574f8bb14f87efea8b4821e5d05 Mon Sep 17 00:00:00 2001
From: Christoph L <47949835+Sir-Photch@users.noreply.github.com>
Date: Thu, 28 Apr 2022 13:49:38 +0200
Subject: [PATCH 27/74] fix: Guarding against empty descriptions in
`SlashCommandBuilder`/`SlashCommandOptionBuilder` (#2260)
* adding null/empty check for option-descriptions
* moving check to Preconditions
* docs
---
.../SlashCommands/SlashCommandBuilder.cs | 27 ++++++-------------
src/Discord.Net.Core/Utils/Preconditions.cs | 17 ++++++++++++
2 files changed, 25 insertions(+), 19 deletions(-)
diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
index ed815ca1a..bf74a160c 100644
--- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
@@ -198,21 +198,13 @@ namespace Discord
string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
- // Make sure the name matches the requirements from discord
- Preconditions.NotNullOrEmpty(name, nameof(name));
- Preconditions.AtLeast(name.Length, 1, nameof(name));
- Preconditions.AtMost(name.Length, MaxNameLength, nameof(name));
+ Preconditions.Options(name, description);
// Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));
- // same with description
- Preconditions.NotNullOrEmpty(description, nameof(description));
- Preconditions.AtLeast(description.Length, 1, nameof(description));
- Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description));
-
// make sure theres only one option with default set to true
if (isDefault == true && Options?.Any(x => x.IsDefault == true) == true)
throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault));
@@ -248,6 +240,7 @@ namespace Discord
throw new InvalidOperationException($"Cannot have more than {MaxOptionsCount} options!");
Preconditions.NotNull(option, nameof(option));
+ Preconditions.Options(option.Name, option.Description); // this is a double-check when this method is called via AddOption(string name... )
Options.Add(option);
return this;
@@ -270,6 +263,9 @@ namespace Discord
if (Options.Count + options.Length > MaxOptionsCount)
throw new ArgumentOutOfRangeException(nameof(options), $"Cannot have more than {MaxOptionsCount} options!");
+ foreach (var option in options)
+ Preconditions.Options(option.Name, option.Description);
+
Options.AddRange(options);
return this;
}
@@ -413,7 +409,7 @@ namespace Discord
MinValue = MinValue,
MaxValue = MaxValue
};
- }
+ }
///
/// Adds an option to the current slash command.
@@ -434,21 +430,13 @@ namespace Discord
string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null,
List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices)
{
- // Make sure the name matches the requirements from discord
- Preconditions.NotNullOrEmpty(name, nameof(name));
- Preconditions.AtLeast(name.Length, 1, nameof(name));
- Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name));
+ Preconditions.Options(name, description);
// Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(name, @"^[\w-]{1,32}$"))
throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name));
- // same with description
- Preconditions.NotNullOrEmpty(description, nameof(description));
- Preconditions.AtLeast(description.Length, 1, nameof(description));
- Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description));
-
// make sure theres only one option with default set to true
if (isDefault && Options?.Any(x => x.IsDefault == true) == true)
throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault));
@@ -483,6 +471,7 @@ namespace Discord
throw new InvalidOperationException($"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!");
Preconditions.NotNull(option, nameof(option));
+ Preconditions.Options(option.Name, option.Description); // double check again
Options.Add(option);
return this;
diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs
index ff8eb7c0d..2f24e660d 100644
--- a/src/Discord.Net.Core/Utils/Preconditions.cs
+++ b/src/Discord.Net.Core/Utils/Preconditions.cs
@@ -297,5 +297,22 @@ namespace Discord
}
}
#endregion
+
+ #region SlashCommandOptions
+
+ ///