From 05120f04280344277f7f4ff4766017938e7507fc Mon Sep 17 00:00:00 2001
From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Date: Mon, 1 Aug 2022 14:19:34 +0300
Subject: [PATCH 01/40] Add AutoServiceScopes to IF docs
---
docs/guides/int_framework/intro.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md
index 54e9086a1..23be5b544 100644
--- a/docs/guides/int_framework/intro.md
+++ b/docs/guides/int_framework/intro.md
@@ -279,8 +279,8 @@ Meaning, the constructor parameters and public settable properties of a module w
For more information on dependency injection, read the [DependencyInjection] guides.
> [!NOTE]
-> On every command execution, module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net.
-> Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the `AfterExecute` method returns.
+> On every command execution, if the 'AutoServiceScopes' option is enabled in the config , module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net.
+> Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the `AfterExecute` method returns. This doesn't apply to methods other than `ExecuteAsync()`.
## Module Groups
From e0d68d47d48b4022c362e7e8bd293fbe0e0d6fd6 Mon Sep 17 00:00:00 2001
From: Wojciech Berdowski <10144015+wberdowski@users.noreply.github.com>
Date: Mon, 1 Aug 2022 13:20:48 +0200
Subject: [PATCH 02/40] Add note about voice binaries on linux
Makes voice section about precompiled binaries more visible.
---
docs/guides/voice/sending-voice.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md
index 555adbca2..36184e3a3 100644
--- a/docs/guides/voice/sending-voice.md
+++ b/docs/guides/voice/sending-voice.md
@@ -17,11 +17,9 @@ bot. (When developing on .NET Framework, this would be `bin/debug`,
when developing on .NET Core, this is where you execute `dotnet run`
from; typically the same directory as your csproj).
-For Windows Users, precompiled binaries are available for your
-convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives).
+**For Windows users, precompiled binaries are available for your convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives).**
-For Linux Users, you will need to compile [Sodium] and [Opus] from
-source, or install them from your package manager.
+**For Linux users, you will need to compile [Sodium] and [Opus] from source, or install them from your package manager.**
[Sodium]: https://download.libsodium.org/libsodium/releases/
[Opus]: http://downloads.xiph.org/releases/opus/
From ee6e0adf7cd5873c2ca886ec85556d3e8d5656fc Mon Sep 17 00:00:00 2001
From: Misha133 <61027276+Misha-133@users.noreply.github.com>
Date: Mon, 1 Aug 2022 14:23:43 +0300
Subject: [PATCH 03/40] Add RequiredInput to example modal (#2348) - Misha-133
---
docs/guides/int_framework/samples/intro/modal.cs | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/docs/guides/int_framework/samples/intro/modal.cs b/docs/guides/int_framework/samples/intro/modal.cs
index 65cc81abf..8a6ba9d8a 100644
--- a/docs/guides/int_framework/samples/intro/modal.cs
+++ b/docs/guides/int_framework/samples/intro/modal.cs
@@ -12,7 +12,9 @@ public class FoodModal : IModal
[ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)]
public string Food { get; set; }
- // Additional paremeters can be specified to further customize the input.
+ // Additional paremeters can be specified to further customize the input.
+ // Parameters can be optional
+ [RequiredInput(false)]
[InputLabel("Why??")]
[ModalTextInput("food_reason", TextInputStyle.Paragraph, "Kuz it's tasty", maxLength: 500)]
public string Reason { get; set; }
@@ -22,10 +24,15 @@ public class FoodModal : IModal
[ModalInteraction("food_menu")]
public async Task ModalResponse(FoodModal modal)
{
+ // Check if "Why??" field is populated
+ string reason = string.IsNullOrWhiteSpace(modal.Reason)
+ ? "."
+ : $" because {modal.Reason}";
+
// Build the message to send.
string message = "hey @everyone, I just learned " +
$"{Context.User.Mention}'s favorite food is " +
- $"{modal.Food} because {modal.Reason}.";
+ $"{modal.Food}{reason}";
// Specify the AllowedMentions so we don't actually ping everyone.
AllowedMentions mentions = new();
From 06ed99512256125c0d32666906feedc2a323a6da Mon Sep 17 00:00:00 2001
From: misticos <21005901+IvMisticos@users.noreply.github.com>
Date: Mon, 1 Aug 2022 13:37:41 +0200
Subject: [PATCH 04/40] docs: Add ServerStarter.Host to deployment.md (#2385)
---
docs/guides/deployment/deployment.md | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/docs/guides/deployment/deployment.md b/docs/guides/deployment/deployment.md
index 0491e841d..4313e85b4 100644
--- a/docs/guides/deployment/deployment.md
+++ b/docs/guides/deployment/deployment.md
@@ -47,6 +47,12 @@ enough. Here is a list of recommended VPS provider.
* Location(s):
* Europe: Lithuania
* Based in: Europe
+* [ServerStarter.Host](https://serverstarter.host/clients/store/discord-bots)
+ * Description: Bot hosting with a panel for quick deployment and
+ no Linux knowledge required.
+ * Location(s):
+ * America: United States
+ * Based in: United States
## .NET Core Deployment
@@ -100,4 +106,4 @@ Windows 10 x64 based machine:
* `dotnet publish -c Release -r win10-x64`
[.NET Core application deployment]: https://docs.microsoft.com/en-us/dotnet/core/deploying/
-[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog
\ No newline at end of file
+[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog
From cf25acdbc10941003046ef097e996b82acd00e4b Mon Sep 17 00:00:00 2001
From: Misha133 <61027276+Misha-133@users.noreply.github.com>
Date: Mon, 1 Aug 2022 14:39:11 +0300
Subject: [PATCH 05/40] docs: Add IgnoreGroupNames clarification to IF docs
(#2374)
---
docs/guides/int_framework/intro.md | 5 +++++
docs/guides/int_framework/samples/intro/groupmodule.cs | 7 ++++++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md
index 23be5b544..5d3253a6f 100644
--- a/docs/guides/int_framework/intro.md
+++ b/docs/guides/int_framework/intro.md
@@ -291,6 +291,11 @@ 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.
+> [!NOTE]
+> To not use the command group's name as a prefix for component or modal interaction's custom id set `ignoreGroupNames` parameter to `true` in classes with [GroupAttribute]
+>
+> However, you have to be careful to prevent overlapping ids of buttons and modals
+
[!code-csharp[Command Group Example](samples/intro/groupmodule.cs)]
## Executing Commands
diff --git a/docs/guides/int_framework/samples/intro/groupmodule.cs b/docs/guides/int_framework/samples/intro/groupmodule.cs
index f0d992aff..a07b2e4d8 100644
--- a/docs/guides/int_framework/samples/intro/groupmodule.cs
+++ b/docs/guides/int_framework/samples/intro/groupmodule.cs
@@ -16,6 +16,11 @@ public class CommandGroupModule : InteractionModuleBase
kyOd-+Eq=1;SMQV8Xy?rGHZVv0t?DCgm+LpE%+1eIMk=?=g1U(q`b?Lt zlRa$adfP!Ma`)-<$4dR(80d`-70xI%ef7%cGoR#R$`5~G6Byr3!S%Xj{%wqlo9tOf zb##oCd913otbPMt2wxACTWbqqAMRH#LG(3(_Y>gr1>~j0^C#kTB&+SO8v#T5zqFk5 z5G^x|b#Yq0bctbleVQt`5#Svb7KYz6U+%*NnNO(p>|+9kaaA?e9PRDCze`o_ty8d$J$z>G&EGKjwK z#qOvY=$zi`>r{H*o2*q@+Nl)`ec0;>2rBnPy1|!Ix+ju*b@ZN-R2+J2C-DXaFHyGj zjj7Iv$xg^awW^rp6(*brBaMx)Zyp?vl*676nuvP}HIh`hr#h8Po)GwC*VWK1xEr=Z z3w5u7EV>fD$Z BQ|IB6e^$$=_-qB(!%g4^>xA=xY;Mj8Mi>EtJQ1`S~9u3OD#XvG;# z>Yubcoh~91h*ZN90pICJKYZx1*V(VoNRIihA%cD11WHchebZId 5h44ARO0MH~F~Z4wNE9;P`fLfG`10(*sD-$&aTU z-`=QewD&7d1>Wkho+SSWKmzP+yjqjBO^cy&^@|+FU5-zU8;|GRnS%`vCSEXQ8iDF} zkHYn=Us!rwgP#{|cM6a873@iibj;op#j!7NU7gj=u*dAk#Ru3 Kcpb ztNWB3?~ky1HNS-AEvg@%e-a9ec0^~hX{}^`rwu7Q`y+iLpn&-r7yqtV%t~&BDt7~( zWad@s(@QVbxa fgVRcLdY&e2JCgRVC8q%(H`F+Tknj{j>g``y_K5D znw*g^IgnEXjaXnH6D5k9u}6Z8kvMT}{(0cZRYB=eJ{NN}Dwi)L#88ybW>b@L72Vhd z>fm4wl57+PdNyOwHFE}L-z~&P)MC^@yDxUGt*E1kJDP%O_1_n3qO~d&ytbyoLqwe4 zo9r1Y1Z&-_ )tT$ =!_i}0Lb29ez` zbbxfJ%gd 8lz z @<8|2+s=O|EP}?jyUg}le&pHE7R1%*$&*}# zas=Un`ux66s=rz7Cpe>J&+cnBO`Oqt7j&Gw!W{8V3PV!zUS3K**iQ8FFgpbXa1=ba z62^R#R7&1=F@0_NFcH_T5}Gsz1Kmb50lLTu?cJ`-w}B=yvsI->h-}imG lo2H-IA9VyJ^>c+Odm!yhp}oKU~yMIUaV&pY?EiRA^u&ONQR{t!>I2nHUQ21iSd zOU0KDJ|oP`?V2~B=W##QCI$VQobL7~z@mW}x>@ENVngt=ZouoVhTM#Oq$&OByi(tt zpB0O)cYKxYik-jHfrg<1j({|MpAm$7bnhIz*RrOKw-Q _i zEfeE0k E?8eEu}$@zf2x!;ey+IUR96z#)dv~L7G_PBb`Jl)G+ >1b@=By^6P_HBy|9mejGNF|t5W5D<++fK|3rd9O?S;e-6r_2tPsmzK@4UQ zG4f>X6OfxyK>uVgoP{kJX5hS &DE?h*?~D6RJyub1kwpX`c>6p?gV z0X2-N6zLhePQxhCn-r9(=H`*gK@^-<)$6+$nUG6wawzNbB`DpzRRFIB#V8RRtpY3E zG* J}7Jb=3TCD5SFqCa72>1XyuxY;+YScOVEi`O`1 zvEg(M%kkVBU~2;owF}@7n*gQ89G}>23BZormhbEk_Ri5j6&5Ez{Y2Ux?jW1E#ZT(m z51Ue2o3!w!b-sfhBZBLxhsP`xzb(9=o>AfYVO4r4i;wnh{Mm;%+M{D8+d#HG@%6KQ zGo+3@07CTJ(ZhxT4kzM|hL(z$)bryvb}W@kO={N?y{rs?1!O7w*|*>HJ4zSu7H1rV zO m`^f%5(jy{eW`u;rV?n*%Cb95yH|<8F zt62BD)Vyn?RiOgU_l#G+(G?Z?sv%i{Slp?RRMSm;F}?VD2D4k&Xt^#gQje3zSi#_- z7F(aJOrlY{w{)ph-FkmDN!NwhE~*2N*1yB?=03LF NSOqZ2yt-?_*cK z!h$%rncuCmsl7n$3b}l91Cl0$>#R6<%*`b(ax&JsN!SGr;lCty`s!NF8w`Svu~bce^g7Y$e~A$HKk zpZ=xUOn)+ ndWKzr_Lsq3TmpU8Vn+piKa>e?ifyA z9{7uRjf#~esD9{-`!{KvVlihl!f-1Kp6+zY4S~FrPnl@$|MZf)hf{?A&OZfD`C1?< z+GqO0|3p{Ii&wb;`ojO_p1+>v2R9yZ{}SaduKVldgV D^}kf{%n@BXCU z21%zR^prUN#*I(s-#A70ZO2;!J =Z1sCPnB_y#cQ! 7)e`$_?=Yic{HMiZD z&i)-8UYY;k(~k%ni0uM;JEcDzFVN~A0w&z*B`=}Q?abeqBfS3~eERdTw;5qLwCeVs zF5Fb{4*}C!@iqSVRo&kiYaqtvAADMy3@<&lQ<}Q`r>5qH{}3=1hNEiWl2?C6M-yJ+ zSGfE)pEkGR`q5sAT$Ch0Eij AH z^^f9&-s@NYoP365;?jSTmFqHgdVOwyjos^nTEbg6gYG?kdYv;JkOl`mgq)_B0U4B1 zvNdd7&f0-bA$b`lZ14TXc9^#YrP)sNKr+*~?3i~vZa4<>I;7I?xQ4Uc_)53jCE`vA zvhScW&`~sW?F&k#>&q@$ptNBK TZI#BQM@Y=Lcffb zrg|#pm36n<`of=Cu~Jnm(nGmypqFBQTu5&|?_db27JJNXO9) KSJuu{BPh@k_by*4khNuqI8kFo!+_dx@){v^B=J1~)MFRZVO}4jqF=lmZGzXU6 zfE98l2N*?KS?I4fc5d8L*0fNEUg?s`+&p!*vU8N9be(yPB;SUK08v@}Xz#S~OAICg zz4h(#%8I~8CLL_G!T1 GRoWk#?<5Z7gB+P3iU}KuME4J7 zvFXPodB-WRmPg{m_DcFfEh+;;Hn #F($VZ2*`erU7M1#xJT$k5R z%!oMN>2>vf_(20!epHRG)pxcxnejo~$pD+=6jy8E)s&1p`bZ5NBzVCzHM+@EA&JKn zZqO=Q;#=SGEd#2MKzIXWVJ)yimzs?KA);2f>Za%D43t+*t`XQWkYpb!t@N~XF1a%1 zDQN3*YGdoIm8d75GB67cvdW4VVZw`-rUPbZ&PYtID4HwiU!Qp3VfiC>W$&4*cJR>L z58C4ZBswPkd&Zi^t^5E6XF>JSd2BpNn>@s^G2YzMyX#7yUi_)+<-!YQn!@$P7jlx> zX(^KJPqS*o)D^-{3~hjIJnULvHNo!f?U3F$t0(zYmpV7LzrJ;3HqIPEdN}1eC(3Bn z#2)W&WsGusGY%=0yretU3 E4%Ls4Ce4ct=30(JK2(qh-&hyeFcFF*P z0fo^8Bba@<^iB2I1I+VMMt(Zi;*D(-C{=$U*Q* 9D>7;PK<5N40Z|gK4^|#Y4&HmP% C-VVR9$7Gq__x`EzZ$*t)_gAf6$u_TOeTb6u*RRWttwHD-&>fW$> zwE;n;9$Eik2#ezc-zyaeUoqXTGt>NvbtUkZpH_y}Mk8O|@_E`?fNIV58SRf`s6O(a z^|QLz`hr-CQ;Kz65@`FJc5X49(zmr^=3yDytl?0J7qUTa@y1ji$S>jRuo;%uY20E} z(7Z>hyeL8(SMIug_c|SK!~&oGDQf*T>EVtw>cw!F0}vY%(oy^^Y`e%Skrh*8*Nv;t zTq3;0mDZ=dL>s@9FOO&8VT!cO0zHVDY!q`T<)Wn>91u}DYEMBx0&u~qKO@f7DW=~< zo7oM~m{IMia6KMgy{_VHR?4py>>UqI6J2-TSzfIzUjW_fjs`<}L{?ZO=Pi1U-r$J1 zYw-btwiJ^4SPUC2TIcIht6x5itZ>6Ix4lJvyuDyS_tk@FkyVHt0MF2B#OrBSl#@_s zh3da1%L*0c%snEw4 _iowr{$XEd&04OTMZYXE z71y@^6bOs7^785e`7=M60%VjDSg63WyugzP-!5@oxo?;TFeTz?{!lKb2;Z9Q1-)Ax zFxq#_CmSd3$+J5`BHRkZx#Un4#Rk>vJ^EuR4!Bzgv$5Gma;xd8KqgG&8CZU%?R!nU zQ)KVRQ|=@_c0Ze%O6G-|?SqRigdhr%%U&-7)Yxq7Uco>J# zGt1siV03VXz-rNg_DX!j(=TD=R(*N()Xk~$`Ho`*Vc!5l%+|-5hx$z1CU>EI4&}I@ zM(Lzv*9V9tu*0kFeaBW<6g4p#p>bTH;Jco74U{XOwLEbDl82>Ik+b)Xb`p7lgm Kg|j% KG9n6*yHoQZRz7_p;xfxp{Sb z7U6J(hU2&ndW6Ox$zyQw*v-*?Fou*W1!>|SRssTA<##43@mpcYrIV8#S@1Zic|B+r zaoCHUW-vVjfQfSuK+>f4kF02yT`jg $&Ri6W%bp(BaV+p#QdA59W(ZH==M$neuEWI|g#d2x c8(e{u u&FY}g@v zvupz)AU;Mg&IVk>PtkYMf||BbAeir^O&Nhd3^W_?{;b+yPM^G<2vaQ!wn+PVU ^9HEDI(A+*_OlLX~=M0k6>;l#C!Bd zetO7k*3Xh~E7$PyK^mb0sYs2FV+rFi)z6Jo*41TI!g{*|`w}C4tRA%G?E7@i`?Pw6 zBRq>!WZLhOeLHBxq9t{s;*ps06zlR*lEHyv>-}=1Dx3!&01#xsp+L&$HlIDvm-g vTuqv9#~>XNd{$gBgF; z_%DXV1UJR?QM1B=)~A5Pg =r1nEKwigBsjxZ_CQM-fUU&$qn0=AUUEgY=R z%_i^>p#jg;JVzF~ta_a_axy MJPt zKDZ8@EH{`@zSLbswD$j!ww{($*R)XrHk6%`sl@z*ciqf{IXeQ;mCi(un)E%IVJL7m zk?>?Z)wqc`rQCwW6WtG}k-p8OL6Z{!;r$w8?pzvP yO&3H4jW`_QEu+=*Fm>p&AK8a-$w|sGr&lSc5fP6QDtS%Ja>qL`R#2< z&Rr`)XRE=hmKChblzGnOg1xK?!-|jhN1 tgNn!J(w4q z+L$o8)r~0ag*hDFOj+fTLnGSe>KJ1OHgC@e0n?xgMkiH(geXbi;Y7O^YqN>fkPptm zYz;#F7}2O2qKLZ`1UT5+sAF}i%_<+bu!Qm>$k-aM&O70kx9r{PIAf2e`gI}D{dMIO z$1sJDLxq?YQ`DQjxUJGi4Mwpng}vPe==OtWX*=h`V(t_Mm`oh`t@rmx$(;wxr@wQT zka7m^=p;+Ne=St7Y;20uM&x(*C!Xy_eevm0MbwE@2QKOx53ze8rzI78#qW?5C-+gK zrg&p~4Gfe5i3hCs%v`#~%Z_e&G-rhEmd3vU@kU3+l_WhuU8%m4nYvExaE->yV>_ey z1$BSz0cnB3FH8IVJCT5++Xuu-I`Rp#*^YBg%0y54xkk6UkyZDv>@rO?wmqVd6gCyt z`M~+ig}bR+?&BvxCa#C(d*YSHN?gqfu&hV>t9kK>!bZb3`<{tndOVGq*Q(u)-vEbJ z74HjE35W@RSl?@Wgry!+TVjNsRIns_@gy1+n(6oZVM}vM+wOqx+BN^=x8RqwNN+=s z#zfNBL*TK9)dPj6sbf!0(7_et!HzremOzwIfRU9Rj(#*}23ZUpgj$~zqC1E`FO128 zyF&txwWrd}0%jn+h2eq8WKFUxjoUgV^Ypavytk7N;xjY5c8P=@6Kj`kml-MS$wZL< zu&{6M16EwL%Bw2G7tSWy@jqWJowgwjCfq>RTN(`8Ag(#y*}Lj*J^jpcL`FcQP%wQO zl-YO8cVx5tf?#XUP9350J}k*mLE|BTu8FezJqW$~cq4vtqYVWM!Lai`wdD2`Y_;lk zc9%=_W}QAjx3z}KMMs`#z2MX?QKlEm38f+tA^WNLh^Luu&hT}Ul4~?>1&?P`KAhAb z`5S*Wu*Zrj*it+xmP9==Qcj?&?In0L$hDq7oXd6$VsO13$K=C%+iP3Nj_iFI@6V6u z5S)L+E_Sp#j>MjC`6ywNWF)O>oJNJ$RF~H}doc34Wg^VQ$o^P1y7-$3cer+~%Yh9B z0y&um?MV ^HJJFGoHLSN8DXPvTkm4e$2IN=tJ-(?L zXqdcM59PSaX9=D<*ekz6l-wJ+PDbePg-D l*OvbFfF*|WQ(j%KSEQ9d)1Mm>36GCe6|Ctqb zd?J4^y|8!}QB;cfWYe$bMFjc3wc7kF^mh?&?(l}RUhHR!IcWvJap{IDbEa=IW^ZYu z-k81irzx?sVEVxU7)@FaiFDe}!(wVONhNR`3_V>TvU=vnmXc~z#P8xVQ_Au&F7`El z*MiK_1ta~&ua@jXmNr0W(n`EYJsq5=S3%we(p+3mQ~e-&f$K7O*HX)R z{t?)2H!Bj*6N4k|U&TW?AcGiJ6x*H8q9PA2fx|#5ODp>P?1WT6?)nvx^ZSVI`uXm6 z?F{chZzkh$B`r1QC1dWgQ(DD1VI9L)s6Z@eVaws@*UpKss)j+Fd{Y#8`N=|RLIp>h zOpJ`JNjo^Eq;m5*;ydJrHvuX{ytgvOSP3>+LpqDMHoK-7dvhdjKM7bH=10FtgvGQl z`#vd7j|p}JI(4%prZduPUkZ}5;D$0y!cU6?yQSA?OOIP?rfV{Y8S!R!*)I7#ob$S> zWa*{_Q> p=HjsR;)Pd+*g5~BW{I`$0}oR*YYOtm1JIj>N5aG~;gj198i$5Q zp3@4dzIT*cbm`VS$5(1CLrGP7TugA5$X|lSRWt+-FkTSKav?Hjcze(IVwGb*Bf0Br zwO_S;wsxGrd3t>HOx7C!ohhL zZR;T{%xCtAnBmcUx6MjoeL7m*x4QX$`noroDB(Ro-~@-Fn1i(JbcVFhvfd>EIr^wV ztHRo$+Ml^Y1kqGkklgJ MXzUg1tiJQBicm`Zc&8B6w2J z?9fUw(kErVd;QYVN?X~^_r;HkO5LU?By2nY`IDi7Gd9F&LrfMetW;jJVl=Ia+4Vo7 z -G1dLN5N3pVdmPCDmI z+QN+ruI(JhJR4-LaEq)@OHmLm7hcti#%ib9f+HLzGvZ3$ICYFo>{mN>daZtM4Kfh4 zHZM?akzyxX2aNpu#+>m|ey-Tyu<843B+8les&;Kt3J)HV-|3N1fB(*+Y%0?``?GY0 zNoP5B9d 1Rhw}@ 2G~#z!y7|Pk`VUDMpK}%m>_xt zc3_d(Xh87cMphU^Xqmps+wo-!T}>BOeJ7Vs4>XB{(O#X;_Dk@D5v>Axf`s>Z%SA{= zfCtcTHj}1_V4a0LLykRH9n4LfX>}1;KVT1cbBedjY(%Jn^*DlS3Z~J%)^S9XEBrP- zi^@~*>Tzn7C^8n1R0?qLPbz2FidxVR$q<47jVpRvSg7(v%U*E(NHqfunSh-AI3BkK z9(SjoC&fEnq4j&UzjPNH$t=RI!|g$t#PMN0_Y0-2*oCfh^UR8$^r#!e(|^G$%8*xy z<<7*^pqs6Y%2M2O)@--0(Pfo!4%d5Uf?GH`;t5luzV;UzH)I0oN6L4{H@vMhD+ZTh zkK;t(pPW`@Wb&<> YiN7Plf`jr0Qdm~utdrqRwL-TSvE z@kdUtdPe7`3HjcI$JlJwY(0)O%e&TMOp+!mTBG>v`YtZ-qv^|}hxVjOvfj41>-4u) z0kQn&lM2F19~UN*9a+qP#m0NL1kszA%Dknp{fMoqm9G6gB20cV Y+Ad zRPZTN*mb=_v{MO^7e+fD?s`vCGRTRVa8CsGIN1cpu-)obn|RJ09Gp`9x<*|Z_Yf|$ z61v~*9$jQ^xhDO`SS;`w*-G+ByJ+{-c}d^1AK!ehie6+*YuJep(rEP(zwfBKm6L&L zb-epuKzWMD4e{vrE TA5mZu^B~qx{ zX^mii?9~WWH?~bp#918ldI?B=TWB<3KtC%^^%$K%@4rb60bGo-`W2)bGvLgVtSa4e z9SHJhAF0bG^H-LYd5`hUfLe{$cZ-5retp)cH$fxk-*Q~=ylu;Kp%criD{n(B4L1n_ z_D?#hNmYqpqc$cf9Cxm-1dx~}h5I?$Lie&FH6Dbj)ED< g*qBvC1w}bcHjx AjUkV(L#qoVpCPMe(Xi^J2^`;^6Cv^VGP@!$FG|nP z7~FPX3aMPTHMCH&_?Ed}tHE*nv*v({BdD=K!Cq?OmGwni^r-lKVV+P#2~5K=y>}~p zV*#cSHr1Iy=Q!ky =no}O$~HzxREH%l^)81S e$I;z8C3t1Cf6?P&F WL zSKZu>YFi&qKG@n>C^$V>?q*K)b%EcnZSAeKaLCS7HkzBfL5ah5d`mc>(x%qX`Hvpf z#Vd4JVNA+>hea {lZKg=Kmk)wiG7{E1i)DK~CyvcoOxIvllUBV4!py3;0J9TFe*?njPI zi1PIMS=P97{pk59fo}<+BPS;;g$)&Jm!aLLAD?Tv1C^Wa%aD&$wdFHrdc$gw^FX!O zn?ic%4UfJ?@AdgRlb7*}?>qU|n>IsPX3pqIb+z;TYomoHL<~>+U1AU q(f{J4I9%7H}Jx!tR>eZ62?1ic-%J@0*4 z^1=xJ=T<68@!z<9(iy|e)Pe{0Q1<966qZ9jkGxGXWrY~Wu5))i)^7f@gW&W=ZkOfW z25kcI&IaS@O(Z<^CA>BB@cUoin^sxwsXVR|DHo2}JJ&pM-@!==&t3d*PX7EeLebin z);NQ|x)F*K&k)HvKHee$#LS2I*~vlY9SIapHmCP~D%?L<)j&Ws)xoxvrabJ=-iWt> zmgby@S~D_JI3eg#UFtyNbQ(f&nl|)UDp~iF<)-)I6mzVoxVFdF-p9|@Bjo?quK2WF z^ek5(I_|HAf?qFxHevj6 K&*>-P^A)))SF@xNgG f|A~3i3H23IlHY=+>-W?CdkQrbP300r^Pv9&$NfzP literal 0 HcmV?d00001 diff --git a/docs/guides/dependency_injection/injection.md b/docs/guides/dependency_injection/injection.md new file mode 100644 index 000000000..c7d40c479 --- /dev/null +++ b/docs/guides/dependency_injection/injection.md @@ -0,0 +1,44 @@ +--- +uid: Guides.DI.Injection +title: Injection +--- + +# Injecting instances within the provider + +You can inject registered services into any class that is registered to the `IServiceProvider`. +This can be done through property or constructor. + +> [!NOTE] +> As mentioned above, the dependency *and* the target class have to be registered in order for the serviceprovider to resolve it. + +## Injecting through a constructor + +Services can be injected from the constructor of the class. +This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class. + +[!code-csharp[Property Injection(samples/property-injecting.cs)]] + +## Injecting through properties + +Injecting through properties is also allowed as follows. + +[!code-csharp[Property Injection](samples/property-injecting.cs)] + +> [!WARNING] +> Dependency Injection will not resolve missing services in property injection, and it will not pick a constructor instead. +> If a publically accessible property is attempted to be injected and its service is missing, the application will throw an error. + +## Using the provider itself + +You can also access the provider reference itself from injecting it into a class. There are multiple use cases for this: + +- Allowing libraries (Like Discord.Net) to access your provider internally. +- Injecting optional dependencies. +- Calling methods on the provider itself if necessary, this is often done for creating scopes. + +[!code-csharp[Provider Injection](samples/provider.cs)] + +> [!NOTE] +> It is important to keep in mind that the provider will pick the 'biggest' available constructor. +> If you choose to introduce multiple constructors, +> keep in mind that services missing from one constructor may have the provider pick another one that *is* available instead of throwing an exception. diff --git a/docs/guides/dependency_injection/samples/access-activator.cs b/docs/guides/dependency_injection/samples/access-activator.cs new file mode 100644 index 000000000..29e71e894 --- /dev/null +++ b/docs/guides/dependency_injection/samples/access-activator.cs @@ -0,0 +1,9 @@ +async Task RunAsync() +{ + //... + + await _serviceProvider.GetRequiredService () + .ActivateAsync(); + + //... +} diff --git a/docs/guides/dependency_injection/samples/collection.cs b/docs/guides/dependency_injection/samples/collection.cs new file mode 100644 index 000000000..4d0457dc9 --- /dev/null +++ b/docs/guides/dependency_injection/samples/collection.cs @@ -0,0 +1,13 @@ +static IServiceProvider CreateServices() +{ + var config = new DiscordSocketConfig() + { + //... + }; + + var collection = new ServiceCollection() + .AddSingleton(config) + .AddSingleton (); + + return collection.BuildServiceProvider(); +} diff --git a/docs/guides/dependency_injection/samples/ctor-injecting.cs b/docs/guides/dependency_injection/samples/ctor-injecting.cs new file mode 100644 index 000000000..c412bd29c --- /dev/null +++ b/docs/guides/dependency_injection/samples/ctor-injecting.cs @@ -0,0 +1,14 @@ +public class ClientHandler +{ + private readonly DiscordSocketClient _client; + + public ClientHandler(DiscordSocketClient client) + { + _client = client; + } + + public async Task ConfigureAsync() + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/enumeration.cs b/docs/guides/dependency_injection/samples/enumeration.cs new file mode 100644 index 000000000..cc8c617f3 --- /dev/null +++ b/docs/guides/dependency_injection/samples/enumeration.cs @@ -0,0 +1,18 @@ +public class ServiceActivator +{ + // This contains *all* registered services of serviceType IService + private readonly IEnumerable _services; + + public ServiceActivator(IEnumerable services) + { + _services = services; + } + + public async Task ActivateAsync() + { + foreach(var service in _services) + { + await service.StartAsync(); + } + } +} diff --git a/docs/guides/dependency_injection/samples/implicit-registration.cs b/docs/guides/dependency_injection/samples/implicit-registration.cs new file mode 100644 index 000000000..52f84228b --- /dev/null +++ b/docs/guides/dependency_injection/samples/implicit-registration.cs @@ -0,0 +1,12 @@ +public static ServiceCollection RegisterImplicitServices(this ServiceCollection collection, Type interfaceType, Type activatorType) +{ + // Get all types in the executing assembly. There are many ways to do this, but this is fastest. + foreach (var type in typeof(Program).Assembly.GetTypes()) + { + if (interfaceType.IsAssignableFrom(type) && !type.IsAbstract) + collection.AddSingleton(interfaceType, type); + } + + // Register the activator so you can activate the instances. + collection.AddSingleton(activatorType); +} diff --git a/docs/guides/dependency_injection/samples/modules.cs b/docs/guides/dependency_injection/samples/modules.cs new file mode 100644 index 000000000..2fadc13d4 --- /dev/null +++ b/docs/guides/dependency_injection/samples/modules.cs @@ -0,0 +1,16 @@ +public class MyModule : InteractionModuleBase +{ + private readonly MyService _service; + + public MyModule(MyService service) + { + _service = service; + } + + [SlashCommand("things", "Shows things")] + public async Task ThingsAsync() + { + var str = string.Join("\n", _service.Things) + await RespondAsync(str); + } +} diff --git a/docs/guides/dependency_injection/samples/program.cs b/docs/guides/dependency_injection/samples/program.cs new file mode 100644 index 000000000..6d985319a --- /dev/null +++ b/docs/guides/dependency_injection/samples/program.cs @@ -0,0 +1,24 @@ +public class Program +{ + private readonly IServiceProvider _serviceProvider; + + public Program() + { + _serviceProvider = CreateProvider(); + } + + static void Main(string[] args) + => new Program().RunAsync(args).GetAwaiter().GetResult(); + + static IServiceProvider CreateProvider() + { + var collection = new ServiceCollection(); + //... + return collection.BuildServiceProvider(); + } + + async Task RunAsync(string[] args) + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/property-injecting.cs b/docs/guides/dependency_injection/samples/property-injecting.cs new file mode 100644 index 000000000..c0c50e150 --- /dev/null +++ b/docs/guides/dependency_injection/samples/property-injecting.cs @@ -0,0 +1,9 @@ +public class ClientHandler +{ + public DiscordSocketClient Client { get; set; } + + public async Task ConfigureAsync() + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/provider.cs b/docs/guides/dependency_injection/samples/provider.cs new file mode 100644 index 000000000..26b600b9d --- /dev/null +++ b/docs/guides/dependency_injection/samples/provider.cs @@ -0,0 +1,26 @@ +public class UtilizingProvider +{ + private readonly IServiceProvider _provider; + private readonly AnyService _service; + + // This service is allowed to be null because it is only populated if the service is actually available in the provider. + private readonly AnyOtherService? _otherService; + + // This constructor injects only the service provider, + // and uses it to populate the other dependencies. + public UtilizingProvider(IServiceProvider provider) + { + _provider = provider; + _service = provider.GetRequiredService (); + _otherService = provider.GetService (); + } + + // This constructor injects the service provider, and AnyService, + // making sure that AnyService is not null without having to call GetRequiredService + public UtilizingProvider(IServiceProvider provider, AnyService service) + { + _provider = provider; + _service = service; + _otherService = provider.GetService (); + } +} diff --git a/docs/guides/dependency_injection/samples/runasync.cs b/docs/guides/dependency_injection/samples/runasync.cs new file mode 100644 index 000000000..d24efc83e --- /dev/null +++ b/docs/guides/dependency_injection/samples/runasync.cs @@ -0,0 +1,17 @@ +async Task RunAsync(string[] args) +{ + // Request the instance from the client. + // Because we're requesting it here first, its targetted constructor will be called and we will receive an active instance. + var client = _services.GetRequiredService (); + + client.Log += async (msg) => + { + await Task.CompletedTask; + Console.WriteLine(msg); + } + + await client.LoginAsync(TokenType.Bot, ""); + await client.StartAsync(); + + await Task.Delay(Timeout.Infinite); +} diff --git a/docs/guides/dependency_injection/samples/scoped.cs b/docs/guides/dependency_injection/samples/scoped.cs new file mode 100644 index 000000000..9942f8d8e --- /dev/null +++ b/docs/guides/dependency_injection/samples/scoped.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddScoped (); + +// Without serviceType: +collection.AddScoped (); diff --git a/docs/guides/dependency_injection/samples/service-registration.cs b/docs/guides/dependency_injection/samples/service-registration.cs new file mode 100644 index 000000000..f6e4d22dd --- /dev/null +++ b/docs/guides/dependency_injection/samples/service-registration.cs @@ -0,0 +1,21 @@ +static IServiceProvider CreateServices() +{ + var config = new DiscordSocketConfig() + { + //... + }; + + // X represents either Interaction or Command, as it functions the exact same for both types. + var servConfig = new XServiceConfig() + { + //... + } + + var collection = new ServiceCollection() + .AddSingleton(config) + .AddSingleton () + .AddSingleton(servConfig) + .AddSingleton (); + + return collection.BuildServiceProvider(); +} diff --git a/docs/guides/dependency_injection/samples/services.cs b/docs/guides/dependency_injection/samples/services.cs new file mode 100644 index 000000000..2e5235b69 --- /dev/null +++ b/docs/guides/dependency_injection/samples/services.cs @@ -0,0 +1,9 @@ +public class MyService +{ + public List Things { get; } + + public MyService() + { + Things = new(); + } +} diff --git a/docs/guides/dependency_injection/samples/singleton.cs b/docs/guides/dependency_injection/samples/singleton.cs new file mode 100644 index 000000000..f395d743e --- /dev/null +++ b/docs/guides/dependency_injection/samples/singleton.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddSingleton (); + +// Without serviceType: +collection.AddSingleton (); diff --git a/docs/guides/dependency_injection/samples/transient.cs b/docs/guides/dependency_injection/samples/transient.cs new file mode 100644 index 000000000..ae1e1a5d8 --- /dev/null +++ b/docs/guides/dependency_injection/samples/transient.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddTransient (); + +// Without serviceType: +collection.AddTransient (); diff --git a/docs/guides/dependency_injection/scaling.md b/docs/guides/dependency_injection/scaling.md new file mode 100644 index 000000000..356fb7c72 --- /dev/null +++ b/docs/guides/dependency_injection/scaling.md @@ -0,0 +1,39 @@ +--- +uid: Guides.DI.Scaling +title: Scaling your DI +--- + +# Scaling your DI + +Dependency injection has a lot of use cases, and is very suitable for scaled applications. +There are a few ways to make registering & using services easier in large amounts. + +## Using a range of services. + +If you have a lot of services that all have the same use such as handling an event or serving a module, +you can register and inject them all at once by some requirements: + +- All classes need to inherit a single interface or abstract type. +- While not required, it is preferred if the interface and types share a method to call on request. +- You need to register a class that all the types can be injected into. + +### Registering implicitly + +Registering all the types is done through getting all types in the assembly and checking if they inherit the target interface. + +[!code-csharp[Registering](samples/implicit-registration.cs)] + +> [!NOTE] +> As seen above, the interfaceType and activatorType are undefined. For our usecase below, these are `IService` and `ServiceActivator` in order. + +### Using implicit dependencies + +In order to use the implicit dependencies, you have to get access to the activator you registered earlier. + +[!code-csharp[Accessing the activator](samples/access-activator.cs)] + +When the activator is accessed and the `ActivateAsync()` method is called, the following code will be executed: + +[!code-csharp[Executing the activator](samples/enumeration.cs)] + +As a result of this, all the services that were registered with `IService` as its implementation type will execute their starting code, and start up. diff --git a/docs/guides/dependency_injection/services.md b/docs/guides/dependency_injection/services.md new file mode 100644 index 000000000..e021a88be --- /dev/null +++ b/docs/guides/dependency_injection/services.md @@ -0,0 +1,48 @@ +--- +uid: Guides.DI.Services +title: Using DI in Interaction & Command Frameworks +--- + +# DI in the Interaction- & Command Service + +For both the Interaction- and Command Service modules, DI is quite straight-forward to use. + +You can inject any service into modules without the modules having to be registered to the provider. +Discord.Net resolves your dependencies internally. + +> [!WARNING] +> The way DI is used in the Interaction- & Command Service are nearly identical, except for one detail: +> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies) + +## Registering the Service + +Thanks to earlier described behavior of allowing already registered members as parameters of the available ctors, +The socket client & configuration will automatically be acknowledged and the XService(client, config) overload will be used. + +[!code-csharp[Service Registration](samples/service-registration.cs)] + +## Usage in modules + +In the constructor of your module, any parameters will be filled in by +the @System.IServiceProvider that you've passed. + +Any publicly settable properties will also be filled in the same +manner. + +[!code-csharp[Module Injection](samples/modules.cs)] + +If you accept `Command/InteractionService` or `IServiceProvider` as a parameter in your constructor or as an injectable property, +these entries will be filled by the `Command/InteractionService` that the module is loaded from and the `IServiceProvider` that is passed into it respectively. + +> [!NOTE] +> Annotating a property with a [DontInjectAttribute] attribute will +> prevent the property from being injected. + +## Services + +Because modules are transient of nature and will reinstantiate on every request, +it is suggested to create a singleton service behind it to hold values across multiple command executions. + +[!code-csharp[Services](samples/services.cs)] + + diff --git a/docs/guides/dependency_injection/types.md b/docs/guides/dependency_injection/types.md new file mode 100644 index 000000000..e539d0695 --- /dev/null +++ b/docs/guides/dependency_injection/types.md @@ -0,0 +1,52 @@ +--- +uid: Guides.DI.Dependencies +title: Types of Dependencies +--- + +# Dependency Types + +There are 3 types of dependencies to learn to use. Several different usecases apply for each. + +> [!WARNING] +> When registering types with a serviceType & implementationType, +> only the serviceType will be available for injection, and the implementationType will be used for the underlying instance. + +## Singleton + +A singleton service creates a single instance when first requested, and maintains that instance across the lifetime of the application. +Any values that are changed within a singleton will be changed across all instances that depend on it, as they all have the same reference to it. + +### Registration: + +[!code-csharp[Singleton Example](samples/singleton.cs)] + +> [!NOTE] +> Types like the Discord client and Interaction/Command services are intended to be singleton, +> as they should last across the entire app and share their state with all references to the object. + +## Scoped + +A scoped service creates a new instance every time a new service is requested, but is kept across the 'scope'. +As long as the service is in view for the created scope, the same instance is used for all references to the type. +This means that you can reuse the same instance during execution, and keep the services' state for as long as the request is active. + +### Registration: + +[!code-csharp[Scoped Example](samples/scoped.cs)] + +> [!NOTE] +> Without using HTTP or libraries like EFCORE, scopes are often unused in Discord bots. +> They are most commonly used for handling HTTP and database requests. + +## Transient + +A transient service is created every time it is requested, and does not share its state between references within the target service. +It is intended for lightweight types that require little state, to be disposed quickly after execution. + +### Registration: + +[!code-csharp[Transient Example](samples/transient.cs)] + +> [!NOTE] +> Discord.Net modules behave exactly as transient types, and are intended to only last as long as the command execution takes. +> This is why it is suggested for apps to use singleton services to keep track of cross-execution data. diff --git a/docs/guides/int_framework/dependency-injection.md b/docs/guides/int_framework/dependency-injection.md deleted file mode 100644 index 31d001f4b..000000000 --- a/docs/guides/int_framework/dependency-injection.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -uid: Guides.IntFw.DI -title: Dependency Injection ---- - -# Dependency Injection - -Dependency injection in the Interaction Service is mostly based on that of the Text-based command service, -for which further information is found [here](xref:Guides.TextCommands.DI). - -> [!NOTE] -> The 2 are nearly identical, except for one detail: -> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index b51aa8088..5cf38bff1 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -374,8 +374,7 @@ delegate can be used to create HTTP responses from a deserialized json object st - Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...). [AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion -[DependencyInjection]: xref:Guides.TextCommands.DI -[Post Execution Docuemntation]: xref:Guides.IntFw.PostExecution +[DependencyInjection]: xref:Guides.DI.Intro [GroupAttribute]: xref:Discord.Interactions.GroupAttribute [InteractionService]: xref:Discord.Interactions.InteractionService diff --git a/docs/guides/text_commands/dependency-injection.md b/docs/guides/text_commands/dependency-injection.md deleted file mode 100644 index 3253643ef..000000000 --- a/docs/guides/text_commands/dependency-injection.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -uid: Guides.TextCommands.DI -title: Dependency Injection ---- - -# Dependency Injection - -The Text Command Service is bundled with a very barebone Dependency -Injection service for your convenience. It is recommended that you use -DI when writing your modules. - -> [!WARNING] -> If you were brought here from the Interaction Service guides, -> make sure to replace all namespaces that imply `Discord.Commands` with `Discord.Interactions` - -## Setup - -1. Create a @Microsoft.Extensions.DependencyInjection.ServiceCollection. -2. Add the dependencies to the service collection that you wish - to use in the modules. -3. Build the service collection into a service provider. -4. Pass the service collection into @Discord.Commands.CommandService.AddModulesAsync* / @Discord.Commands.CommandService.AddModuleAsync* , @Discord.Commands.CommandService.ExecuteAsync* . - -### Example - Setting up Injection - -[!code-csharp[IServiceProvider Setup](samples/dependency-injection/dependency_map_setup.cs)] - -## Usage in Modules - -In the constructor of your module, any parameters will be filled in by -the @System.IServiceProvider that you've passed. - -Any publicly settable properties will also be filled in the same -manner. - -> [!NOTE] -> Annotating a property with a [DontInjectAttribute] attribute will -> prevent the property from being injected. - -> [!NOTE] -> If you accept `CommandService` or `IServiceProvider` as a parameter -> in your constructor or as an injectable property, these entries will -> be filled by the `CommandService` that the module is loaded from and -> the `IServiceProvider` that is passed into it respectively. - -### Example - Injection in Modules - -[!code-csharp[Injection Modules](samples/dependency-injection/dependency_module.cs)] -[!code-csharp[Disallow Dependency Injection](samples/dependency-injection/dependency_module_noinject.cs)] - -[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute diff --git a/docs/guides/text_commands/intro.md b/docs/guides/text_commands/intro.md index 6632c127a..1113b0821 100644 --- a/docs/guides/text_commands/intro.md +++ b/docs/guides/text_commands/intro.md @@ -187,7 +187,7 @@ service provider. ### Module Constructors -Modules are constructed using [Dependency Injection](xref:Guides.TextCommands.DI). Any parameters +Modules are constructed using [Dependency Injection](xref:Guides.DI.Intro). Any parameters that are placed in the Module's constructor must be injected into an @System.IServiceProvider first. diff --git a/docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs b/docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs deleted file mode 100644 index 16ca479db..000000000 --- a/docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs +++ /dev/null @@ -1,65 +0,0 @@ -public class Initialize -{ - private readonly CommandService _commands; - private readonly DiscordSocketClient _client; - - // Ask if there are existing CommandService and DiscordSocketClient - // instance. If there are, we retrieve them and add them to the - // DI container; if not, we create our own. - public Initialize(CommandService commands = null, DiscordSocketClient client = null) - { - _commands = commands ?? new CommandService(); - _client = client ?? new DiscordSocketClient(); - } - - public IServiceProvider BuildServiceProvider() => new ServiceCollection() - .AddSingleton(_client) - .AddSingleton(_commands) - // You can pass in an instance of the desired type - .AddSingleton(new NotificationService()) - // ...or by using the generic method. - // - // The benefit of using the generic method is that - // ASP.NET DI will attempt to inject the required - // dependencies that are specified under the constructor - // for us. - .AddSingleton () - .AddSingleton () - .BuildServiceProvider(); -} -public class CommandHandler -{ - private readonly DiscordSocketClient _client; - private readonly CommandService _commands; - private readonly IServiceProvider _services; - - public CommandHandler(IServiceProvider services, CommandService commands, DiscordSocketClient client) - { - _commands = commands; - _services = services; - _client = client; - } - - public async Task InitializeAsync() - { - // Pass the service provider to the second parameter of - // AddModulesAsync to inject dependencies to all modules - // that may require them. - await _commands.AddModulesAsync( - assembly: Assembly.GetEntryAssembly(), - services: _services); - _client.MessageReceived += HandleCommandAsync; - } - - public async Task HandleCommandAsync(SocketMessage msg) - { - // ... - // Pass the service provider to the ExecuteAsync method for - // precondition checks. - await _commands.ExecuteAsync( - context: context, - argPos: argPos, - services: _services); - // ... - } -} diff --git a/docs/guides/text_commands/samples/dependency-injection/dependency_module.cs b/docs/guides/text_commands/samples/dependency-injection/dependency_module.cs deleted file mode 100644 index 3e42074ca..000000000 --- a/docs/guides/text_commands/samples/dependency-injection/dependency_module.cs +++ /dev/null @@ -1,37 +0,0 @@ -// After setting up dependency injection, modules will need to request -// the dependencies to let the library know to pass -// them along during execution. - -// Dependency can be injected in two ways with Discord.Net. -// You may inject any required dependencies via... -// the module constructor -// -or- -// public settable properties - -// Injection via constructor -public class DatabaseModule : ModuleBase -{ - private readonly DatabaseService _database; - public DatabaseModule(DatabaseService database) - { - _database = database; - } - - [Command("read")] - public async Task ReadFromDbAsync() - { - await ReplyAsync(_database.GetData()); - } -} - -// Injection via public settable properties -public class DatabaseModule : ModuleBase -{ - public DatabaseService DbService { get; set; } - - [Command("read")] - public async Task ReadFromDbAsync() - { - await ReplyAsync(DbService.GetData()); - } -} diff --git a/docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs b/docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs deleted file mode 100644 index 48cd52308..000000000 --- a/docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Sometimes injecting dependencies automatically with the provided -// methods in the prior example may not be desired. - -// You may explicitly tell Discord.Net to **not** inject the properties -// by either... -// restricting the access modifier -// -or- -// applying DontInjectAttribute to the property - -// Restricting the access modifier of the property -public class ImageModule : ModuleBase -{ - public ImageService ImageService { get; } - public ImageModule() - { - ImageService = new ImageService(); - } -} - -// Applying DontInjectAttribute -public class ImageModule : ModuleBase -{ - [DontInject] - public ImageService ImageService { get; set; } - public ImageModule() - { - ImageService = new ImageService(); - } -} From f17866085e308bfdb340fef607fcb406d3e10ada Mon Sep 17 00:00:00 2001 From: Pusheon <59923820+Pusheon@users.noreply.github.com> Date: Tue, 2 Aug 2022 05:24:37 -0400 Subject: [PATCH 15/40] fix: Add DeleteMessagesAsync to IVoiceChannel (#2367) Also adds remaining rate-limit information to client log. --- .../Entities/Channels/IVoiceChannel.cs | 39 +++++++++++++++++++ src/Discord.Net.Rest/BaseDiscordClient.cs | 4 +- .../MockedEntities/MockedVoiceChannel.cs | 3 ++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index d921a2474..d75a4e29c 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -25,6 +26,44 @@ namespace Discord /// int? UserLimit { get; } + /// + /// Bulk-deletes multiple messages. + /// + ///+ /// + ///The following example gets 250 messages from the channel and deletes them. + ///+ /// var messages = await voiceChannel.GetMessagesAsync(250).FlattenAsync(); + /// await voiceChannel.DeleteMessagesAsync(messages); + ///
+ ///+ /// This method attempts to remove the messages specified in bulk. + /// + /// The messages to be bulk-deleted. + /// The options to be used when sending the request. + ///+ /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + ///+ /// A task that represents the asynchronous bulk-removal operation. + /// + Task DeleteMessagesAsync(IEnumerablemessages, RequestOptions options = null); + /// + /// Bulk-deletes multiple messages. + /// + ///+ /// This method attempts to remove the messages specified in bulk. + /// + /// The snowflake identifier of the messages to be bulk-deleted. + /// The options to be used when sending the request. + ///+ /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + ///+ /// A task that represents the asynchronous bulk-removal operation. + /// + Task DeleteMessagesAsync(IEnumerablemessageIds, RequestOptions options = null); + /// /// Modifies this voice channel. /// diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 75f477c7c..af43e9f4e 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -57,7 +57,7 @@ namespace Discord.Rest if (info == null) await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); else - await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); + await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} Remaining: {info.Value.RetryAfter}s {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } @@ -257,6 +257,6 @@ namespace Discord.Rest ///Task IDiscordClient.StopAsync() => Task.Delay(0); - #endregion + #endregion } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index fdbdeda5e..2ffc75a24 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -12,6 +12,9 @@ namespace Discord public int Bitrate => throw new NotImplementedException(); public int? UserLimit => throw new NotImplementedException(); + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => throw new NotImplementedException(); + + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => throw new NotImplementedException(); public ulong? CategoryId => throw new NotImplementedException(); From ba024164216e8abdec31600e4103e7f3ff89f714 Mon Sep 17 00:00:00 2001 From: Alex Thomson Date: Tue, 2 Aug 2022 21:26:34 +1200 Subject: [PATCH 16/40] fix: DisconnectAsync not disconnecting users (#2346) --- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 2 +- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 974ea69ad..3e0ad1840 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1404,7 +1404,7 @@ namespace Discord.Rest /// /// The user to disconnect. /// A task that represents the asynchronous operation for disconnecting a user. - async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = new Optional()); + async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = null); /// async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index cf01857e3..78fb33206 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1407,7 +1407,7 @@ namespace Discord.WebSocket /// /// The user to disconnect. /// A task that represents the asynchronous operation for disconnecting a user. - async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = new Optional()); + async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = null); #endregion #region Stickers From b0b8167efb1b0153913f1251228897c89b178a1f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 3 Aug 2022 10:54:30 +0200 Subject: [PATCH 17/40] fix: Remove group check from RequireContextAttribute (#2409) --- .../Attributes/Preconditions/RequireContextAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs index 9d1cee8d9..057055ffc 100644 --- a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs @@ -58,7 +58,7 @@ namespace Discord.Interactions if ((Contexts & ContextType.Guild) != 0) isValid = !context.Interaction.IsDMInteraction; - if ((Contexts & ContextType.DM) != 0 && (Contexts & ContextType.Group) != 0) + if ((Contexts & ContextType.DM) != 0) isValid = context.Interaction.IsDMInteraction; if (isValid) From c49d4830af625e57ca3f764f4c962cfad25871b0 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 3 Aug 2022 11:11:26 +0200 Subject: [PATCH 18/40] docs: Fix missing entries in TOC (#2415) --- docs/guides/toc.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 45f3983af..c892eb8c4 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -26,6 +26,18 @@ topicUid: Guides.Entities.Casting - name: Glossary & Flowcharts topicUid: Guides.Entities.Glossary +- name: Dependency Injection + items: + - name: Introduction + topicUid: Guides.DI.Intro + - name: Injection + topicUid: Guides.DI.Injection + - name: Command- & Interaction Services + topicUid: Guides.DI.Services + - name: Service Types + topicUid: Guides.DI.Dependencies + - name: Scaling your Application + topicUid: Guides.DI.Scaling - name: Working with Text-based Commands items: - name: Introduction @@ -36,8 +48,6 @@ topicUid: Guides.TextCommands.NamedArguments - name: Preconditions topicUid: Guides.TextCommands.Preconditions - - name: Dependency Injection - topicUid: Guides.TextCommands.DI - name: Post-execution Handling topicUid: Guides.TextCommands.PostExecution - name: Working with the Interaction Framework @@ -50,8 +60,6 @@ topicUid: Guides.IntFw.TypeConverters - name: Preconditions topicUid: Guides.IntFw.Preconditions - - name: Dependency Injection - topicUid: Guides.IntFw.DI - name: Post-execution Handling topicUid: Guides.IntFw.PostExecution - name: Permissions From 1eb42c6128c295c5dcea076d149a0f9ff13797ec Mon Sep 17 00:00:00 2001 From: d4n Date: Wed, 3 Aug 2022 04:17:43 -0500 Subject: [PATCH 19/40] fix: Issues related to absence of bot scope (#2352) --- .../DiscordSocketClient.cs | 14 ++----- .../Channels/SocketCategoryChannel.cs | 2 +- .../Entities/Channels/SocketForumChannel.cs | 2 +- .../Entities/Channels/SocketNewsChannel.cs | 2 +- .../Entities/Channels/SocketStageChannel.cs | 2 +- .../Entities/Channels/SocketTextChannel.cs | 4 +- .../Entities/Channels/SocketVoiceChannel.cs | 6 +-- .../MessageCommands/SocketMessageCommand.cs | 4 +- .../UserCommands/SocketUserCommand.cs | 4 +- .../SocketMessageComponent.cs | 4 +- .../SlashCommands/SocketSlashCommand.cs | 4 +- .../SocketApplicationCommand.cs | 2 +- .../SocketBaseCommand/SocketCommandBase.cs | 4 +- .../SocketBaseCommand/SocketResolvableData.cs | 38 ++++++++++++++----- .../Entities/Messages/SocketUserMessage.cs | 2 +- .../Entities/Roles/SocketRole.cs | 2 +- 16 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3c5621304..f0b50aa8f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2318,7 +2318,7 @@ namespace Discord.WebSocket case "INTERACTION_CREATE": { await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); - + var data = (payload as JToken).ToObject (_serializer); var guild = data.GuildId.IsSpecified ? GetGuild(data.GuildId.Value) : null; @@ -2326,7 +2326,6 @@ namespace Discord.WebSocket if (guild != null && !guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; } SocketUser user = data.User.IsSpecified @@ -2346,15 +2345,8 @@ namespace Discord.WebSocket { channel = CreateDMChannel(data.ChannelId.Value, user, State); } - else - { - if (guild != null) // The guild id is set, but the guild cannot be found as the bot scope is not set. - { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); - return; - } - // The channel isnt required when responding to an interaction, so we can leave the channel null. - } + + // The channel isnt required when responding to an interaction, so we can leave the channel null. } } else if (data.User.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 43f23de1a..42f0c76d4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -39,7 +39,7 @@ namespace Discord.WebSocket } internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); + var entity = new SocketCategoryChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs index bc6e28442..ea58ecdb5 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -34,7 +34,7 @@ namespace Discord.WebSocket internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketForumChannel(guild.Discord, model.Id, guild); + var entity = new SocketForumChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs index eed8f9374..81e152530 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs @@ -23,7 +23,7 @@ namespace Discord.WebSocket } internal new static SocketNewsChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); + var entity = new SocketNewsChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs index 56cd92185..4983bc466 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs @@ -49,7 +49,7 @@ namespace Discord.WebSocket internal new static SocketStageChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketStageChannel(guild.Discord, model.Id, guild); + var entity = new SocketStageChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 6aece7d78..2d8aeeae7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -61,12 +61,12 @@ namespace Discord.WebSocket internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { - if (Discord.MessageCacheSize > 0) + if (Discord?.MessageCacheSize > 0) _messages = new MessageCache(Discord); } internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketTextChannel(guild.Discord, model.Id, guild); + var entity = new SocketTextChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 7bf65d638..9036659fe 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -50,7 +50,7 @@ namespace Discord.WebSocket } internal new static SocketVoiceChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); + var entity = new SocketVoiceChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } @@ -58,8 +58,8 @@ namespace Discord.WebSocket internal override void Update(ClientState state, Model model) { base.Update(state, model); - Bitrate = model.Bitrate.Value; - UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; + Bitrate = model.Bitrate.GetValueOrDefault(64000); + UserLimit = model.UserLimit.GetValueOrDefault() != 0 ? model.UserLimit.Value : (int?)null; RTCRegion = model.RTCRegion.GetValueOrDefault(null); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs index ad5575caa..0c473bcdd 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs @@ -20,9 +20,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketMessageCommandData.Create(client, dataModel, model.Id, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs index c33c06f83..70e06f273 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs @@ -20,9 +20,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketUserCommandData.Create(client, dataModel, model.Id, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index 4f9a769c2..2a1a67d04 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -61,7 +61,9 @@ namespace Discord.WebSocket author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); } else if (model.Message.Value.Author.IsSpecified) - author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); + author = (Channel as SocketChannel)?.GetUser(model.Message.Value.Author.Value.Id); + + author ??= Discord.State.GetOrAddUser(model.Message.Value.Author.Value.Id, _ => SocketGlobalUser.Create(Discord, Discord.State, model.Message.Value.Author.Value)); Message = SocketUserMessage.Create(Discord, Discord.State, author, Channel, model.Message.Value); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs index b3aa4a826..69f733e85 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs @@ -20,9 +20,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketSlashCommandData.Create(client, dataModel, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index 8f27b65f4..f6b3f9699 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -19,7 +19,7 @@ namespace Discord.WebSocket /// Gets whether or not this command is a global application command. /// public bool IsGlobalCommand - => Guild == null; + => GuildId is null; /// public ulong ApplicationId { get; private set; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs index 273f27c9c..bdab128f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -43,9 +43,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketCommandBaseData.Create(client, dataModel, model.Id, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index 98a7daefc..2167a69a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -1,3 +1,4 @@ +using Discord.Net; using System.Collections.Generic; namespace Discord.WebSocket @@ -45,13 +46,24 @@ namespace Discord.WebSocket if (socketChannel == null) { - var channelModel = guild != null - ? discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult() - : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - - socketChannel = guild != null - ? SocketGuildChannel.Create(guild, discord.State, channelModel) - : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); + try + { + var channelModel = guild != null + ? discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id) + .ConfigureAwait(false).GetAwaiter().GetResult() + : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false) + .GetAwaiter().GetResult(); + + socketChannel = guild != null + ? SocketGuildChannel.Create(guild, discord.State, channelModel) + : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); + } + catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions) + { + socketChannel = guildId != null + ? SocketGuildChannel.Create(guild, discord.State, channel.Value) + : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channel.Value); + } } discord.State.AddChannel(socketChannel); @@ -73,7 +85,10 @@ namespace Discord.WebSocket { foreach (var role in resolved.Roles.Value) { - var socketRole = guild.AddOrUpdateRole(role.Value); + var socketRole = guild is null + ? SocketRole.Create(null, discord.State, role.Value) + : guild.AddOrUpdateRole(role.Value); + Roles.Add(ulong.Parse(role.Key), socketRole); } } @@ -93,16 +108,19 @@ namespace Discord.WebSocket author = guild.GetUser(msg.Value.Author.Value.Id); } else - author = (channel as SocketChannel).GetUser(msg.Value.Author.Value.Id); + author = (channel as SocketChannel)?.GetUser(msg.Value.Author.Value.Id); if (channel == null) { - if (!msg.Value.GuildId.IsSpecified) // assume it is a DM + if (guildId is null) // assume it is a DM { channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.State); + author = ((SocketDMChannel)channel).Recipient; } } + author ??= discord.State.GetOrAddUser(msg.Value.Author.Value.Id, _ => SocketGlobalUser.Create(discord, discord.State, msg.Value.Author.Value)); + var message = SocketMessage.Create(discord, discord.State, author, channel, msg.Value); Messages.Add(message.Id, message); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index e5776a089..f5abb2c49 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -127,7 +127,7 @@ namespace Discord.WebSocket refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); } else - refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); + refMsgAuthor = (Channel as SocketChannel)?.GetUser(refMsg.Author.Value.Id); if (refMsgAuthor == null) refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); } diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 1e90b8f5c..b6a61cfb0 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -63,7 +63,7 @@ namespace Discord.WebSocket => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); internal SocketRole(SocketGuild guild, ulong id) - : base(guild.Discord, id) + : base(guild?.Discord, id) { Guild = guild; } From e551431d72838be8a514c417ea98458e6602bdfe Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 3 Aug 2022 16:44:30 +0300 Subject: [PATCH 20/40] Max/Min length fields for ApplicationCommandOption (#2379) * implement max/min length fields for ApplicationCommandOption * fix badly formed xml comments --- .../Interactions/ApplicationCommandOption.cs | 10 ++++ .../Interactions/IApplicationCommandOption.cs | 10 ++++ .../SlashCommands/SlashCommandBuilder.cs | 51 +++++++++++++++++-- .../Attributes/MaxLengthAttribute.cs | 25 +++++++++ .../Attributes/MinLengthAttribute.cs | 25 +++++++++ .../Builders/ModuleClassBuilder.cs | 6 +++ .../SlashCommandParameterBuilder.cs | 36 +++++++++++++ .../Parameters/SlashCommandParameterInfo.cs | 12 +++++ .../Utilities/ApplicationCommandRestUtil.cs | 12 ++++- .../API/Common/ApplicationCommandOption.cs | 10 ++++ .../RestApplicationCommandOption.cs | 9 ++++ .../SocketApplicationCommandOption.cs | 9 ++++ 12 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs index 5857bac81..5e4f6a81d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -81,6 +81,16 @@ namespace Discord /// public double? MaxValue { get; set; } + /// + /// Gets or sets the minimum allowed length for a string input. + /// + public int? MinLength { get; set; } + + ///+ /// Gets or sets the maximum allowed length for a string input. + /// + public int? MaxLength { get; set; } + ////// Gets or sets the choices for string and int types for the user to pick from. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index 72554fc98..c0a752fdc 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -47,6 +47,16 @@ namespace Discord /// double? MaxValue { get; } + ///+ /// Gets the minimum allowed length for a string input. + /// + int? MinLength { get; } + + ///+ /// Gets the maximum allowed length for a string input. + /// + int? MaxLength { get; } + ////// Gets the choices for string and int types for the user to pick from. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index d7d086762..bf22d4e3a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -196,7 +196,7 @@ namespace Discord ///The current builder. public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - Listoptions = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -222,6 +222,8 @@ namespace Discord ChannelTypes = channelTypes, MinValue = minValue, MaxValue = maxValue, + MinLength = minLength, + MaxLength = maxLength, }; return AddOption(option); @@ -354,6 +356,16 @@ namespace Discord /// public double? MaxValue { get; set; } + /// + /// Gets or sets the minimum allowed length for a string input. + /// + public int? MinLength { get; set; } + + ///+ /// Gets or sets the maximum allowed length for a string input. + /// + public int? MaxLength { get; set; } + ////// Gets or sets the choices for string and int types for the user to pick from. /// @@ -377,6 +389,7 @@ namespace Discord { bool isSubType = Type == ApplicationCommandOptionType.SubCommandGroup; bool isIntType = Type == ApplicationCommandOptionType.Integer; + bool isStrType = Type == ApplicationCommandOptionType.String; if (isSubType && (Options == null || !Options.Any())) throw new InvalidOperationException("SubCommands/SubCommandGroups must have at least one option"); @@ -390,6 +403,12 @@ namespace Discord if (isIntType && MaxValue != null && MaxValue % 1 != 0) throw new InvalidOperationException("MaxValue cannot have decimals on Integer command options."); + if(isStrType && MinLength is not null && MinLength < 0) + throw new InvalidOperationException("MinLength cannot be smaller than 0."); + + if (isStrType && MaxLength is not null && MaxLength < 1) + throw new InvalidOperationException("MaxLength cannot be smaller than 1."); + return new ApplicationCommandOptionProperties { Name = Name, @@ -404,7 +423,9 @@ namespace Discord IsAutocomplete = IsAutocomplete, ChannelTypes = ChannelTypes, MinValue = MinValue, - MaxValue = MaxValue + MaxValue = MaxValue, + MinLength = MinLength, + MaxLength = MaxLength, }; } @@ -425,7 +446,7 @@ namespace Discord ///The current builder. public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - Listoptions = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -447,6 +468,8 @@ namespace Discord IsAutocomplete = isAutocomplete, MinValue = minValue, MaxValue = maxValue, + MinLength = minLength, + MaxLength = maxLength, Options = options, Type = type, Choices = (choices ?? Array.Empty ()).ToList(), @@ -669,6 +692,28 @@ namespace Discord return this; } + /// + /// Sets the current builders min length field. + /// + /// The value to set. + ///The current builder. + public SlashCommandOptionBuilder WithMinLength(int length) + { + MinLength = length; + return this; + } + + ///+ /// Sets the current builders max length field. + /// + /// The value to set. + ///The current builder. + public SlashCommandOptionBuilder WithMaxLength(int lenght) + { + MaxLength = lenght; + return this; + } + ////// Sets the current type of this builder. /// diff --git a/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs new file mode 100644 index 000000000..1099e7d92 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + ///+ /// Sets the maximum length allowed for a string type parameter. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class MaxLengthAttribute : Attribute + { + ///+ /// Gets the maximum length allowed for a string type parameter. + /// + public int Length { get; } + + ///+ /// Sets the maximum length allowed for a string type parameter. + /// + /// Maximum string length allowed. + public MaxLengthAttribute(int lenght) + { + Length = lenght; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs new file mode 100644 index 000000000..7d0b0fd63 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + ///+ /// Sets the minimum length allowed for a string type parameter. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class MinLengthAttribute : Attribute + { + ///+ /// Gets the minimum length allowed for a string type parameter. + /// + public int Length { get; } + + ///+ /// Sets the minimum length allowed for a string type parameter. + /// + /// Minimum string length allowed. + public MinLengthAttribute(int lenght) + { + Length = lenght; + } + } +} diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 1bbdfcc4a..35126a674 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -463,6 +463,12 @@ namespace Discord.Interactions.Builders case MinValueAttribute minValue: builder.MinValue = minValue.Value; break; + case MinLengthAttribute minLength: + builder.MinLength = minLength.Length; + break; + case MaxLengthAttribute maxLength: + builder.MaxLength = maxLength.Length; + break; case ComplexParameterAttribute complexParameter: { builder.IsComplexParameter = true; diff --git a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs index d600c9cc7..6f8038cef 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs @@ -28,6 +28,16 @@ namespace Discord.Interactions.Builders /// public double? MinValue { get; set; } + ///+ /// Gets or sets the minimum length allowed for a string type parameter. + /// + public int? MinLength { get; set; } + + ///+ /// Gets or sets the maximum length allowed for a string type parameter. + /// + public int? MaxLength { get; set; } + ////// Gets a collection of the choices of this command. /// @@ -125,6 +135,32 @@ namespace Discord.Interactions.Builders return this; } + ///+ /// Sets + /// New value of the. + /// . + /// + /// The builder instance. + /// + public SlashCommandParameterBuilder WithMinLength(int length) + { + MinLength = length; + return this; + } + + ///+ /// Sets + /// New value of the. + /// . + /// + /// The builder instance. + /// + public SlashCommandParameterBuilder WithMaxLength(int length) + { + MaxLength = length; + return this; + } + ////// Adds parameter choices to diff --git a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs index 8702d69f7..0bce42186 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs @@ -38,6 +38,16 @@ namespace Discord.Interactions /// public double? MaxValue { get; } + ///. /// + /// Gets the minimum length allowed for a string type parameter. + /// + public int? MinLength { get; } + + ///+ /// Gets the maximum length allowed for a string type parameter. + /// + public int? MaxLength { get; } + ////// Gets the that will be used to convert the incoming into /// . @@ -86,6 +96,8 @@ namespace Discord.Interactions Description = builder.Description; MaxValue = builder.MaxValue; MinValue = builder.MinValue; + MinLength = builder.MinLength; + MaxLength = builder.MaxLength; IsComplexParameter = builder.IsComplexParameter; IsAutocomplete = builder.Autocomplete; Choices = builder.Choices.ToImmutableArray(); diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index e4b6f893c..409c0e796 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -23,7 +23,9 @@ namespace Discord.Interactions ChannelTypes = parameterInfo.ChannelTypes?.ToList(), IsAutocomplete = parameterInfo.IsAutocomplete, MaxValue = parameterInfo.MaxValue, - MinValue = parameterInfo.MinValue + MinValue = parameterInfo.MinValue, + MinLength = parameterInfo.MinLength, + MaxLength = parameterInfo.MaxLength, }; parameterInfo.TypeConverter.Write(props, parameterInfo); @@ -209,7 +211,13 @@ namespace Discord.Interactions Name = x.Name, Value = x.Value }).ToList(), - Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList() + Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(), + MaxLength = commandOption.MaxLength, + MinLength = commandOption.MinLength, + MaxValue = commandOption.MaxValue, + MinValue = commandOption.MinValue, + IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), + ChannelTypes = commandOption.ChannelTypes.ToList(), }; public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null) diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs index d703bd46b..fff5730f4 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -38,6 +38,12 @@ namespace Discord.API [JsonProperty("channel_types")] public Optional ChannelTypes { get; set; } + [JsonProperty("min_length")] + public Optional MinLength { get; set; } + + [JsonProperty("max_length")] + public Optional MaxLength { get; set; } + public ApplicationCommandOption() { } public ApplicationCommandOption(IApplicationCommandOption cmd) @@ -56,6 +62,8 @@ namespace Discord.API Default = cmd.IsDefault ?? Optional .Unspecified; MinValue = cmd.MinValue ?? Optional .Unspecified; MaxValue = cmd.MaxValue ?? Optional .Unspecified; + MinLength = cmd.MinLength ?? Optional .Unspecified; + MaxLength = cmd.MaxLength ?? Optional .Unspecified; Autocomplete = cmd.IsAutocomplete ?? Optional .Unspecified; Name = cmd.Name; @@ -77,6 +85,8 @@ namespace Discord.API Default = option.IsDefault ?? Optional .Unspecified; MinValue = option.MinValue ?? Optional .Unspecified; MaxValue = option.MaxValue ?? Optional .Unspecified; + MinLength = option.MinLength ?? Optional .Unspecified; + MaxLength = option.MaxLength ?? Optional .Unspecified; ChannelTypes = option.ChannelTypes?.ToArray() ?? Optional .Unspecified; diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs index 86c6019ed..c47080be7 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -35,6 +35,12 @@ namespace Discord.Rest /// public double? MaxValue { get; private set; } + /// + public int? MinLength { get; private set; } + + /// + public int? MaxLength { get; private set; } + /// /// Gets a collection of @@ -78,6 +84,9 @@ namespace Discord.Rest if (model.Autocomplete.IsSpecified) IsAutocomplete = model.Autocomplete.Value; + MinLength = model.MinLength.ToNullable(); + MaxLength = model.MaxLength.ToNullable(); + Options = model.Options.IsSpecified ? model.Options.Value.Select(Create).ToImmutableArray() : ImmutableArray.Creates for this command. /// (); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs index 27777749a..478c7cb54 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs @@ -33,6 +33,12 @@ namespace Discord.WebSocket /// public double? MaxValue { get; private set; } + /// + public int? MinLength { get; private set; } + + /// + public int? MaxLength { get; private set; } + /// /// Gets a collection of choices for the user to pick from. /// @@ -72,6 +78,9 @@ namespace Discord.WebSocket IsAutocomplete = model.Autocomplete.ToNullable(); + MinLength = model.MinLength.ToNullable(); + MaxLength = model.MaxLength.ToNullable(); + Choices = model.Choices.IsSpecified ? model.Choices.Value.Select(SocketApplicationCommandChoice.Create).ToImmutableArray() : ImmutableArray.Create(); From 02bc3b797745784b41e00789b6da209f3960635f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:05:22 +0200 Subject: [PATCH 21/40] Fix NRE on commandbase data assignment (#2414) --- .../Entities/Interactions/CommandBase/RestCommandBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs index 22e56a733..102ede7b7 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -49,6 +49,9 @@ namespace Discord.Rest internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); + + if (model.Data.IsSpecified && model.Data.Value is RestCommandBaseData data) + Data = data; } /// From 500e7b44caaf9bd47bbbed58a19fbe23dbd8ab64 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 10 Aug 2022 11:37:40 +0300 Subject: [PATCH 22/40] Using RespondWithModalAsync public override string ToString() => Title; private string DebuggerDisplay => $"{Title} ({Type})"; + + public static bool operator ==(Embed left, Embed right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(Embed left, Embed right) + => !(left == right); + + ///() without prior IModal declaration (#2369) * add RespondWithModalAsync method for initializing missing ModalInfos on runtime * update method name and add inline docs --- .../IDiscordInteractionExtensions.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 8f0987661..d970b9930 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -19,9 +19,36 @@ namespace Discord.Interactions if (!ModalUtils.TryGet (out var modalInfo)) throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); + } + + /// + /// Respond to an interaction with a + ///. + /// + /// This method overload uses the + ///parameter to create a new + /// if there isn't a built one already in cache. + /// Type of the + /// The interaction to respond to. + /// Interaction service instance that should be used to buildimplementation. s. + /// The request options for this request. + /// Delegate that can be used to modify the modal. + /// + public static async Task RespondWithModalAsync (this IDiscordInteraction interaction, string customId, InteractionService interactionService, + RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + var modalInfo = ModalUtils.GetOrAdd (interactionService); + + await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); + } + + private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action modifyModal = null) + { var builder = new ModalBuilder(modalInfo.Title, customId); - foreach(var input in modalInfo.Components) + foreach (var input in modalInfo.Components) switch (input) { case TextInputComponentInfo textComponent: From 8dfe19f32892e60ae259a507e4596b76b1b5003f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gutyina=20Gerg=C5=91?= Date: Mon, 15 Aug 2022 19:33:23 +0200 Subject: [PATCH 23/40] Fix placeholder length being hardcoded (#2421) * Fix placeholder length being hardcoded * Add docs for TextInputBuilder.MaxPlaceholderLength --- .../Interactions/MessageComponents/ComponentBuilder.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 37342b039..fd8798ed3 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -1198,6 +1198,10 @@ namespace Discord public class TextInputBuilder { + /// + /// The max length of a + public const int MaxPlaceholderLength = 100; public const int LargestMaxLength = 4000; ///. + /// @@ -1229,13 +1233,13 @@ namespace Discord /// public class ColorTests { + [Fact] public void Color_New() { Assert.Equal(0u, new Color().RawValue); From 89a8ea161fcc7540572e7d272dff8e2069b06195 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:57:51 +0300 Subject: [PATCH 30/40] feat: Embed comparison (#2347) --- .../Entities/Messages/Embed.cs | 39 +++++ .../Entities/Messages/EmbedAuthor.cs | 31 ++++ .../Entities/Messages/EmbedBuilder.cs | 141 ++++++++++++++++++ .../Entities/Messages/EmbedField.cs | 31 ++++ .../Entities/Messages/EmbedFooter.cs | 31 ++++ .../Entities/Messages/EmbedImage.cs | 31 ++++ .../Entities/Messages/EmbedProvider.cs | 31 ++++ .../Entities/Messages/EmbedThumbnail.cs | 31 ++++ .../Entities/Messages/EmbedVideo.cs | 31 ++++ 9 files changed, 397 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index 7fa6f6f36..c1478f56c 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -94,5 +94,44 @@ namespace Discord ////// Gets or sets the placeholder of the current text input. /// - ///+ /// is longer than 100 characters public string Placeholder { get => _placeholder; - set => _placeholder = (value?.Length ?? 0) <= 100 + set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength ? value - : throw new ArgumentException("Placeholder cannot have more than 100 characters."); + : throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters."); } /// is longer than characters From 65b98f8b1251f1cfa64a63c54809a71e4111ad3d Mon Sep 17 00:00:00 2001 From: Bob ///Date: Tue, 16 Aug 2022 03:34:17 +1000 Subject: [PATCH 24/40] Update xmldocs to reflect the ConnectedUsers split (#2418) --- .../Entities/Channels/SocketGuildChannel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 16ed7b32d..808982785 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -36,8 +36,8 @@ namespace Discord.WebSocket /// Gets a collection of users that are able to view the channel. /// - /// If this channel is a voice channel, a collection of users who are currently connected to this channel - /// is returned. + /// If this channel is a voice channel, use ///to retrieve a + /// collection of users who are currently connected to this channel. /// /// A read-only collection of users that can access the channel (i.e. the users seen in the user list). From 6da595e07467c91db3e81e66f7e7bd2b2b9fc7c1 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:52:57 +0300 Subject: [PATCH 25/40] fix ci/cd error (#2428) --- src/Discord.Net.Core/Discord.Net.Core.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 41d83bbc8..005280c4d 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -16,7 +16,6 @@ - all @@ -27,4 +26,7 @@ + + \ No newline at end of file From b6b5e95f48af647531292f3c3c3c53af8f98ef7a Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:53:14 +0200 Subject: [PATCH 26/40] Fix role icon & emoji assignment. (#2416) --- src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 3b2946a0d..2f6d1f062 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -23,7 +23,7 @@ namespace Discord.Rest { role.Guild.Features.EnsureFeature(GuildFeature.RoleIcons); - if (args.Icon.IsSpecified && args.Emoji.IsSpecified) + if ((args.Icon.IsSpecified && args.Icon.Value != null) && (args.Emoji.IsSpecified && args.Emoji.Value != null)) { throw new ArgumentException("Emoji and Icon properties cannot be present on a role at the same time."); } @@ -36,18 +36,18 @@ namespace Discord.Rest Mentionable = args.Mentionable, Name = args.Name, Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create+ (), - Icon = args.Icon.IsSpecified ? args.Icon.Value.Value.ToModel() : Optional .Unspecified, - Emoji = args.Emoji.GetValueOrDefault()?.Name ?? Optional .Unspecified + Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() ?? null : Optional .Unspecified, + Emoji = args.Emoji.IsSpecified ? args.Emoji.Value?.Name ?? "" : Optional.Create (), }; - if (args.Icon.IsSpecified && role.Emoji != null) + if ((args.Icon.IsSpecified && args.Icon.Value != null) && role.Emoji != null) { - apiArgs.Emoji = null; + apiArgs.Emoji = ""; } - if (args.Emoji.IsSpecified && !string.IsNullOrEmpty(role.Icon)) + if ((args.Emoji.IsSpecified && args.Emoji.Value != null) && !string.IsNullOrEmpty(role.Icon)) { - apiArgs.Icon = null; + apiArgs.Icon = Optional .Unspecified; } var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); From b7b7964de97b656579845435c6050104d2c9daf8 Mon Sep 17 00:00:00 2001 From: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Date: Sun, 21 Aug 2022 16:54:19 +0500 Subject: [PATCH 27/40] Fix IGuild.GetBansAsync() (#2424) fix the problem of not being able to get more than 1000 bans --- src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 20140994f..8195a2cea 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -180,7 +180,7 @@ namespace Discord.Rest }, nextPage: (info, lastPage) => { - if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + if (lastPage.Count != DiscordConfig.MaxBansPerBatch) return false; if (dir == Direction.Before) info.Position = lastPage.Min(x => x.User.Id); From 917118d094eb1969c7da6ff8c6e4583c450604f6 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:56:02 +0300 Subject: [PATCH 28/40] [DOCS] Add a note about `DontAutoRegisterAttribute` (#2430) * add a note about `DontAutoRegisterAttribute` * Remove "to to" and add punctuation Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> --- docs/guides/int_framework/intro.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 5cf38bff1..37c579159 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -294,7 +294,7 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can > [!NOTE] > To not use the command group's name as a prefix for component or modal interaction's custom id set `ignoreGroupNames` parameter to `true` in classes with [GroupAttribute] > -> However, you have to be careful to prevent overlapping ids of buttons and modals +> However, you have to be careful to prevent overlapping ids of buttons and modals. [!code-csharp[Command Group Example](samples/intro/groupmodule.cs)] @@ -346,10 +346,13 @@ Command registration methods can only be used after the gateway client is ready Methods like `AddModulesToGuildAsync()`, `AddCommandsToGuildAsync()`, `AddModulesGloballyAsync()` and `AddCommandsGloballyAsync()` can be used to register cherry picked modules or commands to global/guild scopes. +> [!NOTE] +> [DontAutoRegisterAttribute] can be used on module classes to prevent `RegisterCommandsGloballyAsync()` and `RegisterCommandsToGuildAsync()` from registering them to the Discord. + > [!NOTE] > In debug environment, since Global commands can take up to 1 hour to register/update, > it is adviced to register your commands to a test guild for your changes to take effect immediately. -> You can use preprocessor directives to create a simple logic for registering commands as seen above +> You can use preprocessor directives to create a simple logic for registering commands as seen above. ## Interaction Utility @@ -377,6 +380,7 @@ delegate can be used to create HTTP responses from a deserialized json object st [DependencyInjection]: xref:Guides.DI.Intro [GroupAttribute]: xref:Discord.Interactions.GroupAttribute +[DontAutoRegisterAttribute]: xref:Discord.Interactions.DontAutoRegisterAttribute [InteractionService]: xref:Discord.Interactions.InteractionService [InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig [InteractionModuleBase]: xref:Discord.Interactions.InteractionModuleBase From 92215b1f746cdeda4fe53aa0568d078defc5afc9 Mon Sep 17 00:00:00 2001 From: Ge Date: Sun, 21 Aug 2022 19:57:00 +0800 Subject: [PATCH 29/40] fix: Missing Fact attribute in ColorTests (#2425) --- test/Discord.Net.Tests.Unit/ColorTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Discord.Net.Tests.Unit/ColorTests.cs b/test/Discord.Net.Tests.Unit/ColorTests.cs index 46d8feabb..48a6041e5 100644 --- a/test/Discord.Net.Tests.Unit/ColorTests.cs +++ b/test/Discord.Net.Tests.Unit/ColorTests.cs @@ -10,6 +10,7 @@ namespace Discord /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is Embed embed && Equals(embed); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(Embed embed) + => GetHashCode() == embed?.GetHashCode(); + + /// + public override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = hash * 23 + (Type, Title, Description, Timestamp, Color, Image, Video, Author, Footer, Provider, Thumbnail).GetHashCode(); + foreach(var field in Fields) + hash = hash * 23 + field.GetHashCode(); + return hash; + } + } } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index 3b11f6a8b..fdd51e6c9 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -41,5 +42,35 @@ namespace Discord /// /// public override string ToString() => Name; + + public static bool operator ==(EmbedAuthor? left, EmbedAuthor? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedAuthor? left, EmbedAuthor? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedAuthor embedAuthor && Equals(embedAuthor); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedAuthor? embedAuthor) + => GetHashCode() == embedAuthor?.GetHashCode(); + + /// + public override int GetHashCode() + => (Name, Url, IconUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 1e2a7b0d7..db38b9fb7 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -481,6 +481,55 @@ namespace Discord return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable()); } + + public static bool operator ==(EmbedBuilder left, EmbedBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedBuilder left, EmbedBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedBuilder embedBuilder && Equals(embedBuilder); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedBuilder embedBuilder) + { + if (embedBuilder is null) + return false; + + if (Fields.Count != embedBuilder.Fields.Count) + return false; + + for (var i = 0; i < _fields.Count; i++) + if (_fields[i] != embedBuilder._fields[i]) + return false; + + return _title == embedBuilder?._title + && _description == embedBuilder?._description + && _image == embedBuilder?._image + && _thumbnail == embedBuilder?._thumbnail + && Timestamp == embedBuilder?.Timestamp + && Color == embedBuilder?.Color + && Author == embedBuilder?.Author + && Footer == embedBuilder?.Footer + && Url == embedBuilder?.Url; + } + + /// + public override int GetHashCode() => base.GetHashCode(); } /// @@ -597,6 +646,37 @@ namespace Discord /// public EmbedField Build() => new EmbedField(Name, Value.ToString(), IsInline); + + public static bool operator ==(EmbedFieldBuilder left, EmbedFieldBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedFieldBuilder left, EmbedFieldBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedFieldBuilder embedFieldBuilder && Equals(embedFieldBuilder); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedFieldBuilder embedFieldBuilder) + => _name == embedFieldBuilder?._name + && _value == embedFieldBuilder?._value + && IsInline == embedFieldBuilder?.IsInline; + + /// + public override int GetHashCode() => base.GetHashCode(); } /// @@ -697,6 +777,37 @@ namespace Discord /// public EmbedAuthor Build() => new EmbedAuthor(Name, Url, IconUrl, null); + + public static bool operator ==(EmbedAuthorBuilder left, EmbedAuthorBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedAuthorBuilder left, EmbedAuthorBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedAuthorBuilder embedAuthorBuilder && Equals(embedAuthorBuilder); + + /// + /// Determines whether the specified + /// Theis equals to the current + /// to compare with the current + /// + public bool Equals(EmbedAuthorBuilder embedAuthorBuilder) + => _name == embedAuthorBuilder?._name + && Url == embedAuthorBuilder?.Url + && IconUrl == embedAuthorBuilder?.IconUrl; + + /// + public override int GetHashCode() => base.GetHashCode(); } /// @@ -777,5 +888,35 @@ namespace Discord /// public EmbedFooter Build() => new EmbedFooter(Text, IconUrl, null); + + public static bool operator ==(EmbedFooterBuilder left, EmbedFooterBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedFooterBuilder left, EmbedFooterBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedFooterBuilder embedFooterBuilder && Equals(embedFooterBuilder); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedFooterBuilder embedFooterBuilder) + => _text == embedFooterBuilder?._text + && IconUrl == embedFooterBuilder?.IconUrl; + + /// + public override int GetHashCode() => base.GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index f6aa2af3b..1196869fe 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -36,5 +37,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Name; + + public static bool operator ==(EmbedField? left, EmbedField? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedField? left, EmbedField? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current object + ///, will be called to compare the 2 instances + /// + public override bool Equals(object obj) + => obj is EmbedField embedField && Equals(embedField); + + /// + /// Determines whether the specified + /// + ///is equal to the current + /// + public bool Equals(EmbedField? embedField) + => GetHashCode() == embedField?.GetHashCode(); + + /// + public override int GetHashCode() + => (Name, Value, Inline).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 4c507d017..5a1f13158 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -43,5 +44,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Text; + + public static bool operator ==(EmbedFooter? left, EmbedFooter? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedFooter? left, EmbedFooter? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedFooter embedFooter && Equals(embedFooter); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedFooter? embedFooter) + => GetHashCode() == embedFooter?.GetHashCode(); + + /// + public override int GetHashCode() + => (Text, IconUrl, ProxyUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index 9ce2bfe73..85a638dc8 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -53,5 +54,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Url; + + public static bool operator ==(EmbedImage? left, EmbedImage? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedImage? left, EmbedImage? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedImage embedImage && Equals(embedImage); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedImage? embedImage) + => GetHashCode() == embedImage?.GetHashCode(); + + /// + public override int GetHashCode() + => (Height, Width, Url, ProxyUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 960fb3d78..f2ee74613 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -35,5 +36,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Name; + + public static bool operator ==(EmbedProvider? left, EmbedProvider? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedProvider? left, EmbedProvider? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedProvider embedProvider && Equals(embedProvider); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedProvider? embedProvider) + => GetHashCode() == embedProvider?.GetHashCode(); + + /// + public override int GetHashCode() + => (Name, Url).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 7f7b582dc..65c8139c3 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -53,5 +54,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Url; + + public static bool operator ==(EmbedThumbnail? left, EmbedThumbnail? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedThumbnail? left, EmbedThumbnail? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedThumbnail embedThumbnail && Equals(embedThumbnail); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedThumbnail? embedThumbnail) + => GetHashCode() == embedThumbnail?.GetHashCode(); + + /// + public override int GetHashCode() + => (Width, Height, Url, ProxyUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index ca0300e80..0762ed8e7 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -47,5 +48,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Url; + + public static bool operator ==(EmbedVideo? left, EmbedVideo? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedVideo? left, EmbedVideo? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current + ///. + /// + /// If the object passes is an + /// The object to compare with the current, will be called to compare the 2 instances + /// + /// + public override bool Equals(object obj) + => obj is EmbedVideo embedVideo && Equals(embedVideo); + + /// + /// Determines whether the specified + /// Theis equal to the current + /// to compare with the current + /// + public bool Equals(EmbedVideo? embedVideo) + => GetHashCode() == embedVideo?.GetHashCode(); + + /// + public override int GetHashCode() + => (Width, Height, Url).GetHashCode(); } } From ddcf68a29fce08bd59be9e39c5732e4eb1d3a1b2 Mon Sep 17 00:00:00 2001 From: Charlie U <52503242+cpurules@users.noreply.github.com> Date: Sun, 21 Aug 2022 07:58:51 -0400 Subject: [PATCH 31/40] Fix broken code snippet in dependency injection docs (#2420) * Fixed markdown formatting to show code snippet * Fixed constructor injection code snippet pointer --- docs/guides/dependency_injection/injection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/dependency_injection/injection.md b/docs/guides/dependency_injection/injection.md index c7d40c479..85a77476f 100644 --- a/docs/guides/dependency_injection/injection.md +++ b/docs/guides/dependency_injection/injection.md @@ -16,7 +16,7 @@ This can be done through property or constructor. Services can be injected from the constructor of the class. This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class. -[!code-csharp[Property Injection(samples/property-injecting.cs)]] +[!code-csharp[Constructor Injection](samples/ctor-injecting.cs)] ## Injecting through properties From 32b03c8063332d50c93bbf0eefa55044853b6ce1 Mon Sep 17 00:00:00 2001 From: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> Date: Sun, 21 Aug 2022 16:14:55 +0200 Subject: [PATCH 32/40] Added support for lottie stickers (#2359) --- .../API/Rest/CreateStickerParams.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs index b330a0111..a0871bc64 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs @@ -1,4 +1,5 @@ using Discord.Net.Rest; + using System.Collections.Generic; using System.IO; namespace Discord.API.Rest @@ -20,14 +21,21 @@ namespace Discord.API.Rest ["tags"] = Tags }; - string contentType = "image/png"; - + string contentType; if (File is FileStream fileStream) - contentType = $"image/{Path.GetExtension(fileStream.Name)}"; + { + var extension = Path.GetExtension(fileStream.Name).TrimStart('.'); + contentType = extension == "json" ? "application/json" : $"image/{extension}"; + } else if (FileName != null) - contentType = $"image/{Path.GetExtension(FileName)}"; + { + var extension = Path.GetExtension(FileName).TrimStart('.'); + contentType = extension == "json" ? "application/json" : $"image/{extension}"; + } + else + contentType = "image/png"; - d["file"] = new MultipartFile(File, FileName ?? "image", contentType.Replace(".", "")); + d["file"] = new MultipartFile(File, FileName ?? "image", contentType); return d; } From 39bbd298c37a7e2766f9eacb65e768930dee67a9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Fri, 26 Aug 2022 18:45:27 +0300 Subject: [PATCH 33/40] Interactions Command Localization (#2395) * Request headers (#2394) * add support for per-request headers * remove unnecessary usings * Revert "remove unnecessary usings" This reverts commit 8d674fe4faf985b117f143fae3877a1698170ad2. * remove nullable strings from RequestOptions * Add Localization Support to Interaction Service (#2211) * add json and resx localization managers * add utils class for getting command paths * update json regex to make langage code optional * remove IServiceProvider from ILocalizationManager method params * replace the command path method in command map * add localization fields to rest and websocket application command entity implementations * move deconstruct extensions method to extensions folder * add withLocalizations parameter to rest methods * fix build error * add rest conversions to interaction service * add localization to the rest methods * add inline docs * fix implementation bugs * add missing inline docs * inline docs correction (Name/Description Localized properties) * add choice localization * fix conflicts * fix conflicts * add missing command props fields to ToApplicationCommandProps methods * add locale parameter to Get*ApplicationCommandsAsync methods for fetching localized command names/descriptions * Apply suggestions from code review Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * add inline docs to LocalizationTarget * fix upstream merge errors * fix command parsing for context command names with space char * fix command parsing for context command names with space char * fix failed to generate buket id * fix get guild commands endpoint * update rexs localization manager to use single-file pattern * Upstream Merge Localization Branch (#2434) * fix ci/cd error (#2428) * Fix role icon & emoji assignment. (#2416) * Fix IGuild.GetBansAsync() (#2424) fix the problem of not being able to get more than 1000 bans * [DOCS] Add a note about `DontAutoRegisterAttribute` (#2430) * add a note about `DontAutoRegisterAttribute` * Remove "to to" and add punctuation Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> * fix: Missing Fact attribute in ColorTests (#2425) * feat: Embed comparison (#2347) * Fix broken code snippet in dependency injection docs (#2420) * Fixed markdown formatting to show code snippet * Fixed constructor injection code snippet pointer * Added support for lottie stickers (#2359) Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com> Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Co-authored-by: Ge Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com> Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> * remove unnecassary fields from ResxLocalizationManager * update int framework guides * remove space character tokenization from ResxLocalizationManager Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com> Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Co-authored-by: Ge Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com> Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> --- docs/guides/int_framework/intro.md | 41 +++ .../Entities/Guilds/IGuild.cs | 7 +- .../Interactions/ApplicationCommandOption.cs | 92 ++++- .../ApplicationCommandOptionChoice.cs | 33 ++ .../ApplicationCommandProperties.cs | 52 +++ .../ContextMenus/MessageCommandBuilder.cs | 70 ++++ .../ContextMenus/UserCommandBuilder.cs | 72 +++- .../Interactions/IApplicationCommand.cs | 26 ++ .../Interactions/IApplicationCommandOption.cs | 26 ++ .../IApplicationCommandOptionChoice.cs | 15 + .../SlashCommands/SlashCommandBuilder.cs | 325 ++++++++++++++++-- .../Extensions/GenericCollectionExtensions.cs | 15 + src/Discord.Net.Core/IDiscordClient.cs | 4 +- src/Discord.Net.Core/Net/Rest/IRestClient.cs | 10 +- src/Discord.Net.Core/RequestOptions.cs | 12 +- src/Discord.Net.Core/Utils/Preconditions.cs | 10 +- .../InteractionService.cs | 6 + .../InteractionServiceConfig.cs | 5 + .../ILocalizationManager.cs | 32 ++ .../JsonLocalizationManager.cs | 72 ++++ .../ResxLocalizationManager.cs | 55 +++ .../LocalizationTarget.cs | 25 ++ .../Map/CommandMap.cs | 23 +- .../Utilities/ApplicationCommandRestUtil.cs | 88 ++++- .../Utilities/CommandHierarchy.cs | 53 +++ .../API/Common/ApplicationCommand.cs | 13 + .../API/Common/ApplicationCommandOption.cs | 21 ++ .../Common/ApplicationCommandOptionChoice.cs | 7 + .../Rest/CreateApplicationCommandParams.cs | 15 +- .../Rest/ModifyApplicationCommandParams.cs | 7 + src/Discord.Net.Rest/BaseDiscordClient.cs | 2 +- src/Discord.Net.Rest/ClientHelper.cs | 12 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 31 +- src/Discord.Net.Rest/DiscordRestClient.cs | 14 +- .../Entities/Guilds/GuildHelper.cs | 6 +- .../Entities/Guilds/RestGuild.cs | 16 +- .../Interactions/InteractionHelper.cs | 20 +- .../Interactions/RestApplicationCommand.cs | 35 ++ .../RestApplicationCommandChoice.cs | 17 + .../RestApplicationCommandOption.cs | 37 +- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 20 +- .../Net/Queue/Requests/RestRequest.cs | 5 +- .../DiscordSocketClient.cs | 10 +- .../Entities/Guilds/SocketGuild.cs | 11 +- .../SocketApplicationCommand.cs | 35 ++ .../SocketApplicationCommandChoice.cs | 17 + .../SocketApplicationCommandOption.cs | 35 ++ 47 files changed, 1403 insertions(+), 152 deletions(-) create mode 100644 src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationTarget.cs create mode 100644 src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 37c579159..21ea365de 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -376,6 +376,47 @@ respond to the Interactions within your command modules you need to perform the delegate can be used to create HTTP responses from a deserialized json object string. - Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...). +## Localization + +Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`. + +### ResXLocalizationManager + +`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates. + +### JsonLocalizationManager + +`JsonLocaliationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to: + +```json +{ + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + } + }, + "group_1":{ + "name": "localized_name", + "description": "localized_description", + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + }, + "parameter_2":{ + "name": "localized_name", + "description": "localized_description" + } + } + } +} +``` + [AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion [DependencyInjection]: xref:Guides.DI.Intro diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 775ff9e65..34a08f1e7 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1194,12 +1194,17 @@ namespace Discord /// /// Gets this guilds application commands. /// + /// + /// Whether to include full localization dictionaries in the returned objects, + /// instead of the localized name and description fields. + /// + /// The target locale of the localized name and description fields. Sets theX-Discord-Locale header, which takes precedence overAccept-Language . /// The options to be used when sending the request. ////// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of application commands found within the guild. /// - Task> GetApplicationCommandsAsync(RequestOptions options = null); + Task > GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); /// /// Gets an application command within this guild with the specified id. diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs index 5e4f6a81d..bceefda32 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -12,6 +13,8 @@ namespace Discord { private string _name; private string _description; + private IDictionary _nameLocalizations = new Dictionary (); + private IDictionary _descriptionLocalizations = new Dictionary (); /// /// Gets or sets the name of this option. @@ -21,18 +24,7 @@ namespace Discord get => _name; set { - if (value == null) - throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null."); - - if (value.Length > 32) - throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32."); - - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$"); - - if (value.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); - + EnsureValidOptionName(value); _name = value; } } @@ -43,12 +35,11 @@ namespace Discord public string Description { get => _description; - set => _description = value?.Length switch + set { - > 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), - _ => value - }; + EnsureValidOptionDescription(value); + _description = value; + } } /// @@ -105,5 +96,72 @@ namespace Discord /// Gets or sets the allowed channel types for this option. /// public ListChannelTypes { get; set; } + + /// + /// Gets or sets the localization dictionary for the name field of this option. + /// + ///Thrown when any of the dictionary keys is an invalid locale. + public IDictionaryNameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidOptionName(name); + } + _nameLocalizations = value; + } + } + + /// + /// Gets or sets the localization dictionary for the description field of this option. + /// + ///Thrown when any of the dictionary keys is an invalid locale. + public IDictionaryDescriptionLocalizations + { + get => _descriptionLocalizations; + set + { + foreach (var (locale, description) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidOptionDescription(description); + } + _descriptionLocalizations = value; + } + } + + private static void EnsureValidOptionName(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name), $"{nameof(Name)} cannot be null."); + + if (name.Length > 32) + throw new ArgumentOutOfRangeException(nameof(name), "Name length must be less than or equal to 32."); + + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new FormatException($"{nameof(name)} must match the regex ^[\\w-]{{1,32}}$"); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + + private static void EnsureValidOptionDescription(string description) + { + switch (description.Length) + { + case > 100: + throw new ArgumentOutOfRangeException(nameof(description), + "Description length must be less than or equal to 100."); + case 0: + throw new ArgumentOutOfRangeException(nameof(description), "Description length must at least 1."); + } + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs index 6a908b075..8f1ecc6d2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs @@ -1,4 +1,8 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; namespace Discord { @@ -9,6 +13,7 @@ namespace Discord { private string _name; private object _value; + private IDictionary _nameLocalizations = new Dictionary (); /// /// Gets or sets the name of this choice. @@ -40,5 +45,33 @@ namespace Discord _value = value; } } + + /// + /// Gets or sets the localization dictionary for the name field of this choice. + /// + ///Thrown when any of the dictionary keys is an invalid locale. + public IDictionaryNameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException("Key values of the dictionary must be valid language codes."); + + switch (name.Length) + { + case > 100: + throw new ArgumentOutOfRangeException(nameof(value), + "Name length must be less than or equal to 100."); + case 0: + throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1."); + } + } + + _nameLocalizations = value; + } + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 9b3ac8453..7ca16a27d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -1,3 +1,10 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { /// @@ -5,6 +12,9 @@ namespace Discord /// public abstract class ApplicationCommandProperties { + private IReadOnlyDictionary_nameLocalizations; + private IReadOnlyDictionary _descriptionLocalizations; + internal abstract ApplicationCommandType Type { get; } /// @@ -17,6 +27,48 @@ namespace Discord /// public OptionalIsDefaultPermission { get; set; } + /// + /// Gets or sets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionaryNameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + } + _nameLocalizations = value; + } + } + + /// + /// Gets or sets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionaryDescriptionLocalizations + { + get => _descriptionLocalizations; + set + { + foreach (var (locale, description) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } + _descriptionLocalizations = value; + } + } + /// /// Gets or sets whether or not this command can be used in DMs. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs index 59040dd4e..ed49c685d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { ///@@ -31,6 +36,11 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + ///+ /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionaryNameLocalizations => _nameLocalizations; + /// /// Gets or sets whether or not this command can be used in DMs. /// @@ -42,6 +52,7 @@ namespace Discord public GuildPermission? DefaultMemberPermissions { get; set; } private string _name; + private Dictionary_nameLocalizations; /// /// Build the current builder into a IReadOnlyCollectionclass. @@ -86,6 +97,30 @@ namespace Discord return this; } + /// + /// Sets the + /// The localization dictionary to use for the name field of this command. + ///collection. + /// + /// Thrown if + ///is null. Thrown if any dictionary key is an invalid locale string. + public MessageCommandBuilder WithNameLocalizations(IDictionarynameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary (nameLocalizations); + return this; + } + /// /// Sets whether or not this command can be used in dms /// @@ -97,6 +132,41 @@ namespace Discord return this; } + ///+ /// Adds a new entry to the + /// Locale of the entry. + /// Localized string for the name field. + ///collection. + /// The current builder. + ///Thrown if + public MessageCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + private static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... 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)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + ///is an invalid locale string. /// Sets the default member permissions required to use this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs index 7c82dce55..d8bb2e056 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { ///@@ -5,7 +10,7 @@ namespace Discord /// public class UserCommandBuilder { - ///+ /// public bool IsDefaultPermission { get; set; } = true; + ////// Returns the maximum length a commands name allowed by Discord. /// public const int MaxNameLength = 32; @@ -31,6 +36,11 @@ namespace Discord ///+ /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionaryNameLocalizations => _nameLocalizations; + /// /// Gets or sets whether or not this command can be used in DMs. /// @@ -42,6 +52,7 @@ namespace Discord public GuildPermission? DefaultMemberPermissions { get; set; } private string _name; + private Dictionary_nameLocalizations; /// /// Build the current builder into a IReadOnlyCollectionclass. @@ -84,6 +95,30 @@ namespace Discord return this; } + /// + /// Sets the + /// The localization dictionary to use for the name field of this command. + ///collection. + /// The current builder. + ///Thrown if + ///is null. Thrown if any dictionary key is an invalid locale string. + public UserCommandBuilder WithNameLocalizations(IDictionarynameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary (nameLocalizations); + return this; + } + /// /// Sets whether or not this command can be used in dms /// @@ -95,6 +130,41 @@ namespace Discord return this; } + ///+ /// Adds a new entry to the + /// Locale of the entry. + /// Localized string for the name field. + ///collection. + /// The current builder. + ///Thrown if + public UserCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + private static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... 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)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + ///is an invalid locale string. /// Sets the default member permissions required to use this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index 58a002649..6f9ce7a45 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -52,6 +52,32 @@ namespace Discord ///Options { get; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + IReadOnlyDictionaryNameLocalizations { get; } + + /// + /// Gets the localization dictionary for the description field of this command. + /// + IReadOnlyDictionaryDescriptionLocalizations { get; } + + /// + /// Gets the localized name of this command. + /// + ///+ /// Only returned when the `withLocalizations` query parameter is set to + string NameLocalized { get; } + + ///when requesting the command. + /// + /// Gets the localized description of this command. + /// + ///+ /// Only returned when the `withLocalizations` query parameter is set to + string DescriptionLocalized { get; } + ///when requesting the command. + /// /// Modifies the current application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index c0a752fdc..fb179b661 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -71,5 +71,31 @@ namespace Discord /// Gets the allowed channel types for this option. ///ChannelTypes { get; } + + /// + /// Gets the localization dictionary for the name field of this command option. + /// + IReadOnlyDictionaryNameLocalizations { get; } + + /// + /// Gets the localization dictionary for the description field of this command option. + /// + IReadOnlyDictionaryDescriptionLocalizations { get; } + + /// + /// Gets the localized name of this command option. + /// + ///+ /// Only returned when the `withLocalizations` query parameter is set to + string NameLocalized { get; } + + ///when requesting the command. + /// + /// Gets the localized description of this command option. + /// + ///+ /// Only returned when the `withLocalizations` query parameter is set to true when requesting the command. + /// + string DescriptionLocalized { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs index 631706c6f..3f76bae72 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Discord { ///@@ -14,5 +16,18 @@ namespace Discord /// Gets the value of the choice. /// object Value { get; } + + ///+ /// Gets the localization dictionary for the name field of this command option. + /// + IReadOnlyDictionaryNameLocalizations { get; } + + /// + /// Gets the localized name of this command option. + /// + ///+ /// Only returned when the `withLocalizations` query parameter is set to + string NameLocalized { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index bf22d4e3a..579289304 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -1,6 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Net.Sockets; using System.Text.RegularExpressions; namespace Discord @@ -31,18 +34,7 @@ namespace Discord get => _name; set { - Preconditions.NotNullOrEmpty(value, nameof(value)); - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, MaxNameLength, nameof(value)); - - // Discord updated the docs, this regex prevents special characters like @!$%(... etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(value)); - - if (value.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); - + EnsureValidCommandName(value); _name = value; } } @@ -55,10 +47,7 @@ namespace Discord get => _description; set { - Preconditions.NotNullOrEmpty(value, nameof(Description)); - Preconditions.AtLeast(value.Length, 1, nameof(Description)); - Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description)); - + EnsureValidCommandDescription(value); _description = value; } } @@ -76,6 +65,16 @@ namespace Discord } } + ///when requesting the command. + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionaryNameLocalizations => _nameLocalizations; + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionaryDescriptionLocalizations => _descriptionLocalizations; + /// /// Gets or sets whether the command is enabled by default when the app is added to a guild /// @@ -93,6 +92,8 @@ namespace Discord private string _name; private string _description; + private Dictionary_nameLocalizations; + private Dictionary _descriptionLocalizations; private List _options; /// @@ -106,6 +107,8 @@ namespace Discord Name = Name, Description = Description, IsDefaultPermission = IsDefaultPermission, + NameLocalizations = _nameLocalizations, + DescriptionLocalizations = _descriptionLocalizations, IsDMEnabled = IsDMEnabled, DefaultMemberPermissions = DefaultMemberPermissions ?? Optional public bool ExitOnMissingModalField { get; set; } = false; + + ///.Unspecified }; @@ -190,13 +193,17 @@ namespace Discord /// If this option is set to autocomplete. /// The options of the option to add. /// The allowed channel types for this option. + /// Localization dictionary for the name field of this command. + /// Localization dictionary for the description field of this command. /// The choices of this option. /// The smallest number value the user can input. /// The largest number value the user can input. /// The current builder. public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - int? minLength = null, int? maxLength = null, Listoptions = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + List options = null, List channelTypes = null, IDictionary nameLocalizations = null, + IDictionary descriptionLocalizations = null, + int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -226,6 +233,12 @@ namespace Discord MaxLength = maxLength, }; + if (nameLocalizations is not null) + option.WithNameLocalizations(nameLocalizations); + + if (descriptionLocalizations is not null) + option.WithDescriptionLocalizations(descriptionLocalizations); + return AddOption(option); } @@ -268,6 +281,116 @@ namespace Discord Options.AddRange(options); return this; } + + /// + /// Sets the + /// The localization dictionary to use for the name field of this command. + ///collection. + /// + /// Thrown if + ///is null. Thrown if any dictionary key is an invalid locale string. + public SlashCommandBuilder WithNameLocalizations(IDictionarynameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary (nameLocalizations); + return this; + } + + /// + /// Sets the + /// The localization dictionary to use for the description field of this command. + ///collection. + /// + /// Thrown if + ///is null. Thrown if any dictionary key is an invalid locale string. + public SlashCommandBuilder WithDescriptionLocalizations(IDictionarydescriptionLocalizations) + { + if (descriptionLocalizations is null) + throw new ArgumentNullException(nameof(descriptionLocalizations)); + + foreach (var (locale, description) in descriptionLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandDescription(description); + } + + _descriptionLocalizations = new Dictionary (descriptionLocalizations); + return this; + } + + /// + /// Adds a new entry to the + /// Locale of the entry. + /// Localized string for the name field. + ///collection. + /// The current builder. + ///Thrown if + public SlashCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + ///is an invalid locale string. + /// Adds a new entry to the + /// Locale of the entry. + /// Localized string for the description field. + ///collection. + /// The current builder. + ///Thrown if + public SlashCommandBuilder AddDescriptionLocalization(string locale, string description) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandDescription(description); + + _descriptionLocalizations ??= new(); + _descriptionLocalizations.Add(locale, description); + + return this; + } + + internal static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... 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)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + + internal static void EnsureValidCommandDescription(string description) + { + Preconditions.NotNullOrEmpty(description, nameof(description)); + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description)); + } } ///is an invalid locale string. @@ -287,6 +410,8 @@ namespace Discord private string _name; private string _description; + private Dictionary ///_nameLocalizations; + private Dictionary _descriptionLocalizations; /// /// Gets or sets the name of this option. @@ -298,10 +423,7 @@ namespace Discord { if (value != null) { - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value)); - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value)); + EnsureValidCommandOptionName(value); } _name = value; @@ -318,8 +440,7 @@ namespace Discord { if (value != null) { - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value)); + EnsureValidCommandOptionDescription(value); } _description = value; @@ -381,6 +502,16 @@ namespace Discord /// public ListChannelTypes { get; set; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionaryNameLocalizations => _nameLocalizations; + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionaryDescriptionLocalizations => _descriptionLocalizations; + /// /// Builds the current option. /// @@ -424,6 +555,8 @@ namespace Discord ChannelTypes = ChannelTypes, MinValue = MinValue, MaxValue = MaxValue, + NameLocalizations = _nameLocalizations, + DescriptionLocalizations = _descriptionLocalizations, MinLength = MinLength, MaxLength = MaxLength, }; @@ -440,13 +573,17 @@ namespace Discord /// If this option supports autocomplete. /// The options of the option to add. /// The allowed channel types for this option. + /// Localization dictionary for the description field of this command. + /// Localization dictionary for the description field of this command. /// The choices of this option. /// The smallest number value the user can input. /// The largest number value the user can input. ///The current builder. public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - int? minLength = null, int? maxLength = null, Listoptions = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + List options = null, List channelTypes = null, IDictionary nameLocalizations = null, + IDictionary descriptionLocalizations = null, + int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -473,9 +610,15 @@ namespace Discord Options = options, Type = type, Choices = (choices ?? Array.Empty ()).ToList(), - ChannelTypes = channelTypes + ChannelTypes = channelTypes, }; + if(nameLocalizations is not null) + option.WithNameLocalizations(nameLocalizations); + + if(descriptionLocalizations is not null) + option.WithDescriptionLocalizations(descriptionLocalizations); + return AddOption(option); } /// @@ -522,10 +665,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary for to use the name field of this command option choice. ///The current builder. - public SlashCommandOptionBuilder AddChoice(string name, int value) + public SlashCommandOptionBuilder AddChoice(string name, int value, IDictionarynameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -533,10 +677,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary for to use the name field of this command option choice. ///The current builder. - public SlashCommandOptionBuilder AddChoice(string name, string value) + public SlashCommandOptionBuilder AddChoice(string name, string value, IDictionarynameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -544,10 +689,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// Localization dictionary for the description field of this command. ///The current builder. - public SlashCommandOptionBuilder AddChoice(string name, double value) + public SlashCommandOptionBuilder AddChoice(string name, double value, IDictionarynameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -555,10 +701,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary to use for the name field of this command option choice. ///The current builder. - public SlashCommandOptionBuilder AddChoice(string name, float value) + public SlashCommandOptionBuilder AddChoice(string name, float value, IDictionarynameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -566,13 +713,14 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary to use for the name field of this command option choice. ///The current builder. - public SlashCommandOptionBuilder AddChoice(string name, long value) + public SlashCommandOptionBuilder AddChoice(string name, long value, IDictionarynameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } - private SlashCommandOptionBuilder AddChoiceInternal(string name, object value) + private SlashCommandOptionBuilder AddChoiceInternal(string name, object value, IDictionary nameLocalizations = null) { Choices ??= new List (); @@ -594,7 +742,8 @@ namespace Discord Choices.Add(new ApplicationCommandOptionChoiceProperties { Name = name, - Value = value + Value = value, + NameLocalizations = nameLocalizations }); return this; @@ -724,5 +873,107 @@ namespace Discord Type = type; return this; } + + /// + /// Sets the + /// The localization dictionary to use for the name field of this command option. + ///collection. + /// The current builder. + ///Thrown if + ///is null. Thrown if any dictionary key is an invalid locale string. + public SlashCommandOptionBuilder WithNameLocalizations(IDictionarynameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionName(name); + } + + _nameLocalizations = new Dictionary (nameLocalizations); + return this; + } + + /// + /// Sets the + /// The localization dictionary to use for the description field of this command option. + ///collection. + /// The current builder. + ///Thrown if + ///is null. Thrown if any dictionary key is an invalid locale string. + public SlashCommandOptionBuilder WithDescriptionLocalizations(IDictionarydescriptionLocalizations) + { + if (descriptionLocalizations is null) + throw new ArgumentNullException(nameof(descriptionLocalizations)); + + foreach (var (locale, description) in _descriptionLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionDescription(description); + } + + _descriptionLocalizations = new Dictionary (descriptionLocalizations); + return this; + } + + /// + /// Adds a new entry to the + /// Locale of the entry. + /// Localized string for the name field. + ///collection. + /// The current builder. + ///Thrown if + public SlashCommandOptionBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionName(name); + + _descriptionLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + ///is an invalid locale string. + /// Adds a new entry to the + /// Locale of the entry. + /// Localized string for the description field. + ///collection. + /// The current builder. + ///Thrown if + public SlashCommandOptionBuilder AddDescriptionLocalization(string locale, string description) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionDescription(description); + + _descriptionLocalizations ??= new(); + _descriptionLocalizations.Add(locale, description); + + return this; + } + + private static void EnsureValidCommandOptionName(string name) + { + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + } + + private static void EnsureValidCommandOptionDescription(string description) + { + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } } } diff --git a/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs new file mode 100644 index 000000000..75d81d292 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace System.Collections.Generic; + +internal static class GenericCollectionExtensions +{ + public static void Deconstructis an invalid locale string. (this KeyValuePair kvp, out T1 value1, out T2 value2) + { + value1 = kvp.Key; + value2 = kvp.Value; + } + + public static Dictionary ToDictionary (this IEnumerable > kvp) => + kvp.ToDictionary(x => x.Key, x => x.Value); +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 14e156769..dd1da3ae3 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -155,12 +155,14 @@ namespace Discord /// /// Gets a collection of all global commands. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. SetsX-Discord-Locale header, which takes precedence overAccept-Language . /// The options to be used when sending the request. ////// A task that represents the asynchronous get operation. The task result contains a read-only collection of global /// application commands. /// - Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null); + Task > GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); /// /// Creates a global application command. diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index 71010f70d..d28fb707e 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -30,9 +30,13 @@ namespace Discord.Net.Rest /// The cancellation token used to cancel the task. /// Indicates whether to send the header only. /// The audit log reason. + /// Additional headers to be sent with the request. /// ///- Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); - Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); - Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable >> requestHeaders = null); + Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable >> requestHeaders = null); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable >> requestHeaders = null); } } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 46aa2681f..ef8dbf756 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -1,5 +1,6 @@ using Discord.Net; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,7 @@ namespace Discord /// Gets or sets the maximum time to wait for this request to complete. /// - /// Gets or set the max time, in milliseconds, to wait for this request to complete. If + /// Gets or set the max time, in milliseconds, to wait for this request to complete. If /// @@ -53,7 +54,7 @@ namespace Discord ///null , a request will not time out. If a rate limit has been triggered for this request's bucket /// and will not be unpaused in time, this request will fail immediately. ////// This property can also be set in @@ -70,8 +71,10 @@ namespace Discord internal bool IsReactionBucket { get; set; } internal bool IsGatewayBucket { get; set; } + internal IDictionary. - /// On a per-request basis, the system clock should only be disabled + /// On a per-request basis, the system clock should only be disabled /// when millisecond precision is especially important, and the /// hosting system is known to have a desynced clock. /// > RequestHeaders { get; } + internal static RequestOptions CreateOrClone(RequestOptions options) - { + { if (options == null) return new RequestOptions(); else @@ -96,8 +99,9 @@ namespace Discord public RequestOptions() { Timeout = DiscordConfig.DefaultRequestTimeout; + RequestHeaders = new Dictionary >(); } - + public RequestOptions Clone() => MemberwiseClone() as RequestOptions; } } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 2f24e660d..fb855f925 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -55,7 +55,7 @@ namespace Discord if (obj.Value == null) throw CreateNotNullException(name, msg); if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } - } + } private static ArgumentException CreateNotEmptyException(string name, string msg) => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); @@ -129,7 +129,7 @@ namespace Discord private static ArgumentException CreateNotEqualException (string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); - + /// Value must be at least public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } ///. Value must be at least @@ -165,7 +165,7 @@ namespace Discord private static ArgumentException CreateAtLeastException. (string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); - + /// Value must be greater than public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } ///. Value must be greater than @@ -201,7 +201,7 @@ namespace Discord private static ArgumentException CreateGreaterThanException. (string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); - + /// Value must be at most public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } ///. Value must be at most @@ -237,7 +237,7 @@ namespace Discord private static ArgumentException CreateAtMostException. (string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); - + /// Value must be less than public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } ///. Value must be less than diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 793d89cdc..50c1f5546 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -83,6 +83,11 @@ namespace Discord.Interactions public event Func. ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } internal readonly AsyncEvent > _modalCommandExecutedEvent = new(); + /// + /// Get the + public ILocalizationManager LocalizationManager { get; set; } + private readonly ConcurrentDictionaryused by this Interaction Service instance to localize strings. + /// _typedModuleDefs; private readonly CommandMap _slashCommandMap; private readonly ConcurrentDictionary > _contextCommandMaps; @@ -203,6 +208,7 @@ namespace Discord.Interactions _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; + LocalizationManager = config.LocalizationManager; _typeConverterMap = new TypeMap (this, new ConcurrentDictionary { diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs index b6576a49f..b9102bc5f 100644 --- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs +++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs @@ -64,6 +64,11 @@ namespace Discord.Interactions /// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value. /// + /// Localization provider to be used when registering application commands. + /// + public ILocalizationManager LocalizationManager { get; set; } } ///diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs new file mode 100644 index 000000000..13b155292 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Respresents a localization provider for Discord Application Commands. + /// + public interface ILocalizationManager + { + ///+ /// Get every the resource name for every available locale. + /// + /// Location of the resource. + /// Type of the resource. + ///+ /// A dictionary containing every available locale and the resource name. + /// + IDictionaryGetAllNames(IList key, LocalizationTarget destinationType); + + /// + /// Get every the resource description for every available locale. + /// + /// Location of the resource. + /// Type of the resource. + ///+ /// A dictionary containing every available locale and the resource name. + /// + IDictionaryGetAllDescriptions(IList key, LocalizationTarget destinationType); + } +} diff --git a/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs new file mode 100644 index 000000000..010fb3bdd --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// The default localization provider for Json resource files. + /// + public sealed class JsonLocalizationManager : ILocalizationManager + { + private const string NameIdentifier = "name"; + private const string DescriptionIdentifier = "description"; + private const string SpaceToken = "~"; + + private readonly string _basePath; + private readonly string _fileName; + private readonly Regex _localeParserRegex = new Regex(@"\w+.(?\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline); + + /// + /// Initializes a new instance of the + /// Base path of the Json file. + /// Name of the Json file. + public JsonLocalizationManager(string basePath, string fileName) + { + _basePath = basePath; + _fileName = fileName; + } + + ///class. + /// + public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) => + GetValues(key, DescriptionIdentifier); + + /// + public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) => + GetValues(key, NameIdentifier); + + private string[] GetAllFiles() => + Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly); + + private IDictionary GetValues(IList key, string identifier) + { + var result = new Dictionary (); + var files = GetAllFiles(); + + foreach (var file in files) + { + var match = _localeParserRegex.Match(Path.GetFileName(file)); + if (!match.Success) + continue; + + var locale = match.Groups["locale"].Value; + + using var sr = new StreamReader(file); + using var jr = new JsonTextReader(sr); + var obj = JObject.Load(jr); + var token = string.Join(".", key.Select(x => $"['{x}']")) + $".{identifier}"; + var value = (string)obj.SelectToken(token); + if (value is not null) + result[locale] = value; + } + + return result; + } + } +} diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs new file mode 100644 index 000000000..a110602f2 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Resources; + +namespace Discord.Interactions +{ + /// + /// The default localization provider for Resx files. + /// + public sealed class ResxLocalizationManager : ILocalizationManager + { + private const string NameIdentifier = "name"; + private const string DescriptionIdentifier = "description"; + + private readonly ResourceManager _resourceManager; + private readonly IEnumerable_supportedLocales; + + /// + /// Initializes a new instance of the + /// Name of the base resource. + /// The main assembly for the resources. + /// Cultures theclass. + /// should search for. + public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales) + { + _supportedLocales = supportedLocales; + _resourceManager = new ResourceManager(baseResource, assembly); + } + + /// + public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) => + GetValues(key, DescriptionIdentifier); + + /// + public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) => + GetValues(key, NameIdentifier); + + private IDictionary GetValues(IList key, string identifier) + { + var entryKey = (string.Join(".", key) + "." + identifier); + + var result = new Dictionary (); + + foreach (var locale in _supportedLocales) + { + var value = _resourceManager.GetString(entryKey, locale); + if (value is not null) + result[locale.Name] = value; + } + + return result; + } + } +} diff --git a/src/Discord.Net.Interactions/LocalizationTarget.cs b/src/Discord.Net.Interactions/LocalizationTarget.cs new file mode 100644 index 000000000..cf54d3375 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationTarget.cs @@ -0,0 +1,25 @@ +namespace Discord.Interactions +{ + /// + /// Resource targets for localization. + /// + public enum LocalizationTarget + { + ///+ /// Target is a + Group, + ///tagged with a . + /// + /// Target is an application command method. + /// + Command, + ///+ /// Target is a Slash Command parameter. + /// + Parameter, + ///+ /// Target is a Slash Command parameter choice. + /// + Choice + } +} diff --git a/src/Discord.Net.Interactions/Map/CommandMap.cs b/src/Discord.Net.Interactions/Map/CommandMap.cs index 2e7bf5368..336e2b1ec 100644 --- a/src/Discord.Net.Interactions/Map/CommandMap.cs +++ b/src/Discord.Net.Interactions/Map/CommandMap.cs @@ -42,7 +42,7 @@ namespace Discord.Interactions public void RemoveCommand(T command) { - var key = ParseCommandName(command); + var key = CommandHierarchy.GetCommandPath(command); _root.RemoveCommand(key, 0); } @@ -60,28 +60,9 @@ namespace Discord.Interactions private void AddCommand(T command) { - var key = ParseCommandName(command); + var key = CommandHierarchy.GetCommandPath(command); _root.AddCommand(key, 0, command); } - - private IListParseCommandName(T command) - { - var keywords = new List () { command.Name }; - - var currentParent = command.Module; - - while (currentParent != null) - { - if (!string.IsNullOrEmpty(currentParent.SlashGroupName)) - keywords.Add(currentParent.SlashGroupName); - - currentParent = currentParent.Parent; - } - - keywords.Reverse(); - - return keywords; - } } } diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 409c0e796..9b507f1bb 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Discord.Interactions @@ -9,6 +10,9 @@ namespace Discord.Interactions #region Parameters public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo) { + var localizationManager = parameterInfo.Command.Module.CommandService.LocalizationManager; + var parameterPath = parameterInfo.GetParameterPath(); + var props = new ApplicationCommandOptionProperties { Name = parameterInfo.Name, @@ -18,12 +22,15 @@ namespace Discord.Interactions Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { Name = x.Name, - Value = x.Value + Value = x.Value, + NameLocalizations = localizationManager?.GetAllNames(parameterInfo.GetChoicePath(x), LocalizationTarget.Choice) ?? ImmutableDictionary .Empty })?.ToList(), ChannelTypes = parameterInfo.ChannelTypes?.ToList(), IsAutocomplete = parameterInfo.IsAutocomplete, MaxValue = parameterInfo.MaxValue, MinValue = parameterInfo.MinValue, + NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary .Empty, + DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary .Empty, MinLength = parameterInfo.MinLength, MaxLength = parameterInfo.MaxLength, }; @@ -38,13 +45,19 @@ namespace Discord.Interactions public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo) { + var commandPath = commandInfo.GetCommandPath(); + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var props = new SlashCommandBuilder() { Name = commandInfo.Name, Description = commandInfo.Description, + IsDefaultPermission = commandInfo.DefaultPermission, IsDMEnabled = commandInfo.IsEnabledInDm, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), - }.Build(); + }.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary .Empty) + .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary .Empty) + .Build(); if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); @@ -54,18 +67,30 @@ namespace Discord.Interactions return props; } - public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) => - new ApplicationCommandOptionProperties + public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) + { + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var commandPath = commandInfo.GetCommandPath(); + + return new ApplicationCommandOptionProperties { Name = commandInfo.Name, Description = commandInfo.Description, Type = ApplicationCommandOptionType.SubCommand, IsRequired = false, - Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() + Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps()) + ?.ToList(), + NameLocalizations = localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary .Empty, + DescriptionLocalizations = localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary .Empty }; + } public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) - => commandInfo.CommandType switch + { + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var commandPath = commandInfo.GetCommandPath(); + + return commandInfo.CommandType switch { ApplicationCommandType.Message => new MessageCommandBuilder { @@ -73,16 +98,21 @@ namespace Discord.Interactions IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm - }.Build(), + } + .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary .Empty) + .Build(), ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm - }.Build(), + } + .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary .Empty) + .Build(), _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") }; + } #endregion #region Modules @@ -123,6 +153,9 @@ namespace Discord.Interactions options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister))); + var localizationManager = moduleInfo.CommandService.LocalizationManager; + var modulePath = moduleInfo.GetModulePath(); + var props = new SlashCommandBuilder { Name = moduleInfo.SlashGroupName, @@ -130,7 +163,10 @@ namespace Discord.Interactions IsDefaultPermission = moduleInfo.DefaultPermission, IsDMEnabled = moduleInfo.IsEnabledInDm, DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions - }.Build(); + } + .WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary .Empty) + .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary .Empty) + .Build(); if (options.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); @@ -168,7 +204,11 @@ namespace Discord.Interactions Name = moduleInfo.SlashGroupName, Description = moduleInfo.Description, Type = ApplicationCommandOptionType.SubCommandGroup, - Options = options + Options = options, + NameLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group) + ?? ImmutableDictionary .Empty, + DescriptionLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group) + ?? ImmutableDictionary .Empty, } }; } @@ -183,17 +223,29 @@ namespace Discord.Interactions Name = command.Name, Description = command.Description, IsDefaultPermission = command.IsDefaultPermission, - Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional >.Unspecified + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional
>.Unspecified, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary
.Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary .Empty, }, ApplicationCommandType.User => new UserCommandProperties { Name = command.Name, - IsDefaultPermission = command.IsDefaultPermission + IsDefaultPermission = command.IsDefaultPermission, + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary .Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary .Empty }, ApplicationCommandType.Message => new MessageCommandProperties { Name = command.Name, - IsDefaultPermission = command.IsDefaultPermission + IsDefaultPermission = command.IsDefaultPermission, + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary .Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary .Empty }, _ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"), }; @@ -206,18 +258,20 @@ namespace Discord.Interactions Description = commandOption.Description, Type = commandOption.Type, IsRequired = commandOption.IsRequired, + ChannelTypes = commandOption.ChannelTypes?.ToList(), + IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), + MinValue = commandOption.MinValue, + MaxValue = commandOption.MaxValue, Choices = commandOption.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { Name = x.Name, Value = x.Value }).ToList(), Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(), + NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(), + DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary(), MaxLength = commandOption.MaxLength, MinLength = commandOption.MinLength, - MaxValue = commandOption.MaxValue, - MinValue = commandOption.MinValue, - IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), - ChannelTypes = commandOption.ChannelTypes.ToList(), }; public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null) diff --git a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs new file mode 100644 index 000000000..a4554eaef --- /dev/null +++ b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Interactions +{ + internal static class CommandHierarchy + { + public const char EscapeChar = '$'; + + public static IList GetModulePath(this ModuleInfo moduleInfo) + { + var result = new List (); + + var current = moduleInfo; + while (current is not null) + { + if (current.IsSlashGroup) + result.Insert(0, current.SlashGroupName); + + current = current.Parent; + } + + return result; + } + + public static IList GetCommandPath(this ICommandInfo commandInfo) + { + if (commandInfo.IgnoreGroupNames) + return new string[] { commandInfo.Name }; + + var path = commandInfo.Module.GetModulePath(); + path.Add(commandInfo.Name); + return path; + } + + public static IList GetParameterPath(this IParameterInfo parameterInfo) + { + var path = parameterInfo.Command.GetCommandPath(); + path.Add(parameterInfo.Name); + return path; + } + + public static IList GetChoicePath(this IParameterInfo parameterInfo, ParameterChoice choice) + { + var path = parameterInfo.GetParameterPath(); + path.Add(choice.Name); + return path; + } + + public static IList GetTypePath(Type type) => + new string[] { EscapeChar + type.FullName }; + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 8b84149dd..e46369277 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API { @@ -25,6 +26,18 @@ namespace Discord.API [JsonProperty("default_permission")] public Optional DefaultPermissions { get; set; } + [JsonProperty("name_localizations")] + public Optional > NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional > DescriptionLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } + + [JsonProperty("description_localized")] + public Optional DescriptionLocalized { get; set; } + // V2 Permissions [JsonProperty("dm_permission")] public Optional DmPermission { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs index fff5730f4..fb64d5ebe 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; using System.Linq; namespace Discord.API @@ -38,6 +39,18 @@ namespace Discord.API [JsonProperty("channel_types")] public Optional ChannelTypes { get; set; } + [JsonProperty("name_localizations")] + public Optional > NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional > DescriptionLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } + + [JsonProperty("description_localized")] + public Optional DescriptionLocalized { get; set; } + [JsonProperty("min_length")] public Optional MinLength { get; set; } @@ -69,6 +82,11 @@ namespace Discord.API Name = cmd.Name; Type = cmd.Type; Description = cmd.Description; + + NameLocalizations = cmd.NameLocalizations?.ToDictionary() ?? Optional >.Unspecified; + DescriptionLocalizations = cmd.DescriptionLocalizations?.ToDictionary() ?? Optional >.Unspecified; + NameLocalized = cmd.NameLocalized; + DescriptionLocalized = cmd.DescriptionLocalized; } public ApplicationCommandOption(ApplicationCommandOptionProperties option) { @@ -94,6 +112,9 @@ namespace Discord.API Type = option.Type; Description = option.Description; Autocomplete = option.IsAutocomplete; + + NameLocalizations = option.NameLocalizations?.ToDictionary() ?? Optional >.Unspecified; + DescriptionLocalizations = option.DescriptionLocalizations?.ToDictionary() ?? Optional >.Unspecified; } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs index 6f84437f6..966405cc9 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API { @@ -9,5 +10,11 @@ namespace Discord.API [JsonProperty("value")] public object Value { get; set; } + + [JsonProperty("name_localizations")] + public Optional > NameLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index 7ae8718b6..2257d4b97 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -1,4 +1,8 @@ using Newtonsoft.Json; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Discord.API.Rest { @@ -19,6 +23,12 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + [JsonProperty("name_localizations")] + public Optional > NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional > DescriptionLocalizations { get; set; } + [JsonProperty("dm_permission")] public Optional DmPermission { get; set; } @@ -26,12 +36,15 @@ namespace Discord.API.Rest public Optional DefaultMemberPermission { get; set; } public CreateApplicationCommandParams() { } - public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) + public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, + IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null) { Name = name; Description = description; Options = Optional.Create(options); Type = type; + NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional >.Unspecified; + DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional >.Unspecified; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs index 5891c2c28..f49a3f33d 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Rest { @@ -15,5 +16,11 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + + [JsonProperty("name_localizations")] + public Optional > NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional > DescriptionLocalizations { get; set; } } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index af43e9f4e..686c7b030 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -243,7 +243,7 @@ namespace Discord.Rest => Task.FromResult (null); /// - Task > IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + Task > IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) => Task.FromResult >(ImmutableArray.Create ()); Task IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) => Task.FromResult (null); diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index c6ad6a9fb..0c8f8c42f 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -194,10 +194,10 @@ namespace Discord.Rest }; } - public static async Task > GetGlobalApplicationCommandsAsync(BaseDiscordClient client, - RequestOptions options = null) + public static async Task > GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false, + string locale = null, RequestOptions options = null) { - var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); + var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); if (!response.Any()) return Array.Empty (); @@ -212,10 +212,10 @@ namespace Discord.Rest return model != null ? RestGlobalCommand.Create(client, model) : null; } - public static async Task > GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, - RequestOptions options = null) + public static async Task > GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, bool withLocalizations = false, + string locale = null, RequestOptions options = null) { - var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false); + var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, withLocalizations, locale, options).ConfigureAwait(false); if (!response.Any()) return ImmutableArray.Create