From ecc6e884c444047243d6752d7a10948b90bcc9ed Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 13 Apr 2016 01:36:37 -0700 Subject: [PATCH 01/25] Support Gateway v4 This adds support for gateway v4 (which we're technically already running; whoops). Really it just moves the version to the URL, and explicitly uses JSON as the encoding. Everything should be supported from previous commits. --- wsapi.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wsapi.go b/wsapi.go index 765b705..7a65394 100644 --- a/wsapi.go +++ b/wsapi.go @@ -15,6 +15,7 @@ import ( "compress/zlib" "encoding/json" "errors" + "fmt" "io" "log" "net/http" @@ -25,6 +26,8 @@ import ( "github.com/gorilla/websocket" ) +var GATEWAY_VERSION int = 4 + type handshakeProperties struct { OS string `json:"$os"` Browser string `json:"$browser"` @@ -34,7 +37,6 @@ type handshakeProperties struct { } type handshakeData struct { - Version int `json:"v"` Token string `json:"token"` Properties handshakeProperties `json:"properties"` LargeThreshold int `json:"large_threshold"` @@ -68,6 +70,9 @@ func (s *Session) Open() (err error) { return } + // Add the version and encoding to the URL + g = g + fmt.Sprintf("?v=%v&encoding=json", GATEWAY_VERSION) + header := http.Header{} header.Add("accept-encoding", "zlib") @@ -78,7 +83,7 @@ func (s *Session) Open() (err error) { return } - err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{3, s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) + err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) if err != nil { return } From 37ede2b51f0b226b580b684279c5d8536f5ac4b8 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 26 Apr 2016 08:38:13 -0500 Subject: [PATCH 02/25] Add mkdocs structure --- docs/img/discordgo.png | Bin 0 -> 34331 bytes docs/index.md | 5 +++++ mkdocs.yml | 13 +++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 docs/img/discordgo.png create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/docs/img/discordgo.png b/docs/img/discordgo.png new file mode 100644 index 0000000000000000000000000000000000000000..eb15a3dc0a02cfdce0d3ec413b17841c76c059f4 GIT binary patch literal 34331 zcmce-WlUv3w=KG9+#R}ccXxMpcehPLU>K;!P-xVyW%d*kk~x#2rGIXCy+e=qN? zq<+j)s-}!otu^MHD?&*@5)lpu4gdfkN=u2U003a|UjqgT008)Rb4&mLaEd=gMU|8+ z?VRnLEbZ)xq(w!E>>ceae*Q8C0NmHIRDo)$Cs_PXTlXTe;r>aob}E=KL@FXt{ul`q zG{mqdGU3E|tC-5YsN&)fxFfmY;IXm((U{6~2$66r&vvz_TwO zA1z4_bQb{3eI6bL&?BGG!-N<1iF_)OrU&L94A$eEz$FXgj{y+!j1?&c2uXnXXQfi8 z0&*b$M&qU?`v4_+fDxtN(LBIE>p9H}44|7tj0u*T03bp)4;KSi@&YQSH6q0UT1)^8 z3)ul~zzRKpNm|oF3h=iX&@+VsUk89e05Bs@#O2#c7pbJCq0IjLXKtMGmjZKTeVf@WFU5KI2Bkc!E02}ho-2?!Tn}CVB z6Lk0CK7~|0HN_Fzgk(s6+70zdVq&rnI-IVs7XkowoqcCPbd2>l{=88BHlTd+O9)E? z_*|E#D2qB|p+-RN$(rV!{a0+n^Ws}qR}T*kR%8Z5bdAR}eL%oI!ye6#UoXCV?@tdq zU3+8!EcyYG5YIb(6ZdjOc+nOeQle-%_v}3Z0JrUSy$e(@;Qp3D zyVGu;SArjsx#R$U3+dku0DyrQ8H3tbz2Gnm03endK=W4^_rCW#T@MsqFXToq{JRlH zun5^ep9qQwoS8qKqY-swpa@my|Uyl z09Xj&5g2r1;KAl}I%rbt(K?e#dXz zVR{m63A`f!mcaMW4oT*ufI4OV1%yV?)>2NKV2OPA88c_*^f;{Cj2XuU9R6sIyyKY) zOXRgU0nuTmo`2t=SqFYGF%0#K)N;|mCYp}dl2js84B`CMs0441crcx=b%Q_)5buHI zfN&EbGA0m~(vs6sSdl6v!XsltU4!L>1rJv4qa;Zzm2IGCLwy!AOD^#J#P_*NG#%S6bb@L6?_N@dJS=Eot*A=@F8c`A#rdO^C%W@(-pDtnxcNL5~w zN{w<@DLR`=I`?D{mb7e9Reth3jwQC`)6bfJ&TXih$pi{1Gn#YbCzAhC{(;}SAs~ig z1`US;seWX9hyD)y;r6{IRj&BQeAJI&H6~8xv()1>c=bE=akaQI167}D33XQ0{W92vKNafbSxRTBxT?T1-qNsgV5yy&V4<(} zM+B61zg)kfXrXd$t>J#h5D~oTk9v1HVr#^TP?rGq0gBqmRc}|C_S6sj_9*`HvOI24 zRvA{!dX>8*3qk1pREOtGX9d_}s*;pD-^eYDE9yOJ++sGlR%UZO>+(M-f3zy3DwHcE z&pJ7(FG{kCJH^|z-vW??hh-@BC?$b$Q;=E5s;jD-C2%EJ^87px)ePmiC1NCyqkj(R?UU`>4A~47P(V=-Q^?An%3r3d$hQ_#7xT*T z$_-CWOy*B6rLkx5GcU6oXB=lNXKXhc>A36AHI+8Cm`m3k=oD*THg(vB=x}QnX{qbz z*Gp7oRgo{XSGAX*mb5A)DpX`I0sH^nG>tZuHUIc~xB|6a-DK0`V+=IG1~v~?_g^M< zCi=Ab#C-5WBZjRaRp9I3`#Lo^V(@72Y^6_SUJCjQ-v2pa1ZqvAOs}x+H=&PH5B$th z&6t-2Vgp;9g6_hjB4h){8BzooGH{1BM>-E=oF5$YcRlAGvNGy3`YuY>Yc4r1e_n=d z*TP-kniI?eQti0&vK&I-ckBr{oWSNbZ7Dh2=>k4YI#8518%}WcG>g-`<`* zy@|VH1$oxZAL~{xgrjp?g)xQW0;>WKd#i{oN6zD`h@XyArxK=qB84dV{#%-XHa;?r z>i0#U3<(R-3>A#E57)w(Wlmvto81s~+7^2i`y;lM0c2L>uA!GsM;qy(#s0gaC5 zkC0rr0$0+tFbYj6Ry6r7x*K^coJSviACQD`mSZ-2MO2T5K;c%3M&9%Hc|r%f2iYKN z5#3>HFlAYMha95ZW(H3-R=O*Z4GTX{Cv(v6gm?#$a$@zUcO9RD$2#0TCM0dB22>?qOJoy`&7xw(g=R{%ZzqMPymf#< z?^s}OBbfCWygJ-rh|k%^@`b*l7Nhodqw#BK7OnthL}WF_I(7=C-wM@+`yplwl312@|2?#BwNJQwFbm(jfRVdNAY#Wlu4@f_Mj%w)`D)n zmJaih(WyyZLA64)d$m_Jj?1~tM&spY+u2K8uZ~By^Tl=Dj;9ZS(XMrMFm?Gwi$(J{ z>9_hP!Rmr&X z4n*~s%%MF}dohoLlPB2iCjvyCHP>jb%3bJrLRWksKBJXL`fa*IhHCnou8D4&;)KBL z(&?7xyW~0{awZ#}wuj7}+UU=e7*_0qGm(aYlL z1UH*=YV*F+;|A+)9z{T>0ujd)7O#mV`{v8)s6D9szb5E@LuRbcvW;W zY*A1bw6Suv;b+z3mU}n5A^8Lq5;P4;c!)z5g^Wv!8xKVcJFbo@AZFA<$K zo2JOd2IYgstFf^deV-T4mZxS@XY*2SQxdyvyo)|gd(M8;tPk}K5p<2ZvoagpYrI|b zx^1}OUlnX4D;{_g`B8mZzSKUb&Xl|*{zy#xgaA*ydpBjJUk3mHP$sgHVu1fVFQV;! z0002iUP{{u0DwpT8eo9*%Ef+% z_&I#DoORaw8xxm#<-2~+EE7)=B&wK@vfoN%fL1s?%`GN)q1yMXIrcVFx^SC=Z&;OX z%P$|ysYJhmML8Zl1PBDY?YEBwcs~S|U9!%2XvCFKL|}+7lXW-(P()y0QHX<4(1=7} zh{=S4QADKtVg8?TJOEV$h8T?Gzvurq`I{IFaWF*q*N*?0{~!5(HvX^q|B?T{Q1pLQ z{2xXCSH=J3!T%WYe|q$P%K!23KOX%5q3Hi*DEZWMRw42W$;ip6wY9a)%*~Bi(xl|& zA$x{~pm1?R6Z0+e$qU85#+1=lN!T5HvJ2h?!M|&!elmpo6aw z3rUXXHH4FOHEZnIDZsK_mKxhK`=RSfN?(dZB|wuj70d zkT<)dLJD|lLo8G4M!j^w-BFwTtQO7Z%GmY@`B!B8eml+>O~9}7?SO&*VL^55e6`-8 z+#o0*3M67}qiS-eL5DTBuaJo65Ph&ODaGTPn)FX<0;*qr{^4{nYATC&z8E%YNBWfEwtHp$pL>xiP(pqRh)Y6Vx=TQ*j&G#5GeBObOP2QHjKuT#zBopS( z&8fv&Q^_`$v!Pt!pzE8Pgbyk(5(05)Y3Z@|mpkoFPj+06UsUgUKc{)a^w`6V@PMYW~oZ zLdwe>QD_?0;=T7>lZ3Pd?}zYIvKe*2h9vAjS-POQXuC?4D(A-80`y}4s`t^m_f)9e z(S#vw#C9E+sjb4UTbv+>t3VD2OdAaov*-(=;IJ^KogOd%nm+&(GJ#i}6fuC$-1N$GZ7Tl`tgyBzoN@ z4A6RSJIf-kTZv~%wp#h}bzVpWHDk&%JuY{vBQvnsj=_7b!e+h6`D`f$nB&vrIT(p) zYK|=9AFg6G7>PMn(+wIMOQwYvbSnEHRRt1-`Ns;VfWstQ+2%Mhb8@(1n7Y~+3fbH( z9L(-I@lsS&OlP;2NTiUroXG`?!C~*4pTEo7%(Aes_??qO==yNF7@#!MeVCJVZCki# z)k@%ugoO0dv$Jcozm4c}rB+nC&TLek69({@y%l1-H=BDS3ZGax0FjUNDZP*{I4aYj z_WV++T;Tq&s0j-P_gf_bBAf~lLwvXWs!R0`z3xzx%{pLabw#BBXc!2EG}vhy1pV0T zq{4vS4q}|DG!*X(=+v_PC-(2`O!jQ4>SUn|{bId&czO9LK#vUp3Ot3;P=mpsL(0U2 zWcf2EJ>7Ht69EcbTU(p_@u{k^@^5{8|LSU5Bqr_N!9j3+J*$F(LQ8x5{_bu-NeQK` zt*yI<$L`6AcK!#=R5*isA(Vp?9uwpu!w06A(A}T5>BsL-=;VFVoWNv^cJnwaK z!$uPAfbDbCytStNNzYaWPm#;TTJqz?`h2Ss5QoiDi3A(EV0L7+9+=$H!d3l=jf*Sm z?ac?A&L*d(KIuBDsBaW4ST5*(OiGimQ~%YNJ!m$Ns;V?>H!;=si)~}U-~&(F{2a@Z5A?N+sJTxn@B(HsTW)pR?j^)TAs3v~&J4B>C6qT#^==@u*()Hl+$ z+9c-0F>{8!M^_veo}Fa5v1h$kXpYKvir9{s*Ci&7$jcKSJF={fMl|17JeTC=5`6;) zhlPX7SIFXlfP_RvL9tg{cYAxh7$fw17j}2&N=Qh6q-@iP(VRb8IA4sj%JSm-E7@@6 z4DR?Ir!Dr*ozc;-(1#m`ZQve|&F>`>FXbN&z1tT8KPE}&Gr-vW?r2j}c5SnnO<`)- zS74k7eaoq;Ac5X`?WRzzCjwf$@0)&mIFs!5{rJviIpgynqaZ7L=6*1efG8P@w>y(3 ziYMURGLoxg-N5DUwZC%_Woj;n*XaZ2+2y4<)H{x=i`^WivFzvfzyrzO%gU%vM7xWY z28%t)ML4}!f1}5bt!U9VND_wgb0!xI5tpM-I0%Z(b|dxm;cR;m9A-R)QS)dzXZro+ z4qkBn--8X?a9}07|{UJGFp`k2BJphU5?|F5# zt#4b?5(NpS!?Dy}kLSiumz(EXof#`Dy6f5O8X8>c6#=U3ewpRV17%h&ndNV)wK6UX zhnD{k>U)KTUffltGf6Hcs8tjP<2_%=P8>??tW4Xg9rlNUgM(4f&_pUUsv>QyS#VHe%oY>h zaX5atYf?H{SWv7t+iALyx36^hywT5VadYkIRB%)WxY$?eLpSTVlxj^9HcEv4 zV2m6zBY99`OCcp~LGuY5r;JTN!}n0#hNp6O&l8Wr3JwmQsWX>7U9Rz4=SYsiW~Au` zz0ziI+A(ZM(!@Y*Yh1wKJVW@}h{~Fcq}>k}uskqvUNR6B+zPLeBGh zpdVU_&oMqY=*`yi*JImOr`LR*D0G|iiHMQW4*i?+$Lqsb^bb}TczD|PiKWRbe*th< z9Ihm|E_G17%b0ZQM8HqgaD+GfsMX+-5=zF7>u*WI|#Nf9Q3K<+6B^Co?#~ zcV=^$Ck=gYYl?}WUx$d-l`f0Nh33(0v(u}&&hw8V{uxhcKmNI#AA|ii{ zDxKLfRmwK!6GzqKt_sbX+i}KjD(j`n7q9MeHFxMRrKgAlwTqd<0KP(pb=(YA%2n1w zr#+4;$gc~&<93X8ZiWGj1axvRnm85=i>}pTPYl5Hu0XjRf+%-6f6Rdx{x@6TahR+b zdA^hU`}ghYTBG%_A#zMTa2m_Eq_JtJKp4Fvqj7+iD_G_MS4Lfp`nMl@7QC0MqwlcB z)D;rt-QG(^*VRlOxXoHao??ywC%)3L(gvZ0^(Ql)mtJQg2?K%mY?n34YMiJ&&f zX;&BK^MJoPI|kfnB%W9DHOS;&R%W%@@ma`W&mUqHHf++}JD<#7W4{zDEta z4f@gYUm_g4*&M&eR_SdtxBu+~gUi0P+H7P0czr0)Z+E4kvULC44I@lC98bY^Ra?~2 zRjmgKo(i%b3F4wY|2xm&zTS8H?o@YkbF&-6xwSKhZJ0C{4k7iz2Ask>T(|2!yx#3E zU9kH0hLpc5yI<=v!$vZY?de&>&=>Am3plz zY0Rcj<0*{Yu7^s>Mf#FBv7SiNLz4v0w-)3ECU25gDBVRyY$pbtp3LTB80DHZ48kN} zDgpyTLo_wrARgHi2FP5M4~d3srZOY7MKrUC(W4ngL3`&V#?)K5A$QNmRp86>ew-a# zY=LAPmMd3#?NeQ2BC9&DbFogz<#TYu9~B91dvcn<6+~;UPFKtm2OylF-CQATm2R`e zznMI?pL1fE`1nE*4AeVhQ2R{q3qU6_V@pk0h!hibN3;dWm~Op;1@e-IS$GA zIbDhqb%Cz^0zR+T`*DH-3t&9tLIdCq*E_<8n{M0Yn{6%ti2ENxQJj<}`{ycqZt?2^ z-Wi1jVg7;HopzlMST^fT>W2phOu#8T^OxJhO8xexYfg?S2DPiTsC51}l``Lg&ko5t zs*m4sgu4sn=9xO3(V0o(AvsT%n;Dcj-oL@Xp-Q`6PieoHKyX+X_zs#<)Q&@qYNKch zG!EH~E@z#~9`1pj!cc#I5M$SKAZ3oXiip^^gIRl1($<1FMw_iqjBSkznMYeX)}T&R zf&GENRw^ubL|hKDjTVP!JZ_an^Kq=NLwXn|2%^d~0Gi4!#_E^N)-xvSHoPeEMn}}i zGG}l%JK@gCVxx3>`0S<{5EBzO*lwoRnhw{Q48mbC=#5Ne@q*dIm&?|t4DYYEeSqs$ zcigizbv;4V;(mTJ8VE<7NM#l+N4~{oIgLjA>X3h zn^;p1R!moN2R@(IPv7o_5wbWP$t_juzqv7*Hsa*OFMY*&afX4MCpVYM@3y8Gz1|yD zIqH+_(9C)FIFA4Edx1m@2?a%H2s}0x67$u18$bQ9qIa&Mjd2L|5(b#yAcof$A zvP_VJX zbPC8VXV4+KpNGKDB!9aeVc;70Wz)_kXlZLZ?DT-0E|ifHlYptb>bE&ddtA02!)*%^ zuwBFI#O8!gRht>E;xxJov%>^yM4>LvMPvR3+4la;(nlO#JtA|GCME z@&ad1Fd^lS%)*`^H-{a?s&oW^&1NlWW=2UxMFmdscay6l#X+N6>EhK5^qhdn)HLvI z_zI=bU7)M03y;@TPCZWqa#a|Os7O;xEO@S3pI1di<*$oV$;Yta?lMP%*r$ZTn0b?e zcjEKp8Q6C=OB7U8)Q~H$zfaeNQ}Kb>eH;wcb*clR#>{D4^{(WSz)um!f!V(4;piWt zj*gDTf@oJeC~H3N58$}CHV-SinVU@!$^{ZPXEoh2)MDQX@2wU}6C9tfjo;fJ@-NO~_j$oQH5*TU&Y^&u%4*HO0$AG^Z=#p{S@uGJU;1cv%mu4jIdsu4ecG z-dt}_SClj~a9LSju#;Ktpp%1|=l7?Wy4|4ZDA2tg&-1kFEzn5Nh>E5c%2boF4ZRa& z9&l&Svj545s-JbuX@%t@{Tj+y<;iu&N9HdsX1RygrQ%Y90e^t<*U>=zA zPl-FUQV#TQDKwLIbRv^GF@aRF{Pyl{wcD?|&gobo7LQxx*DpG1YU+cdqx@M*dwcty z{{A4HMk`c2Jh}BIo1444#Onk=YyW1~=s+YUZPIEzu-@fNgN23VZ}q>gL-Lqypru|K zE>ks>nkee3V$y3Z&8RP?Ot<3Y$@;6m8m&SkQoA_n)4wpcyUg z(J3@@9gRNmN5u@0iMo&wax{Rhrkf{-1$nt=jr_4WA8Q&o9=u$t+swf4d2jS`dnhI@ zo~L5{_vyZ`cO1H1hTz{X8)Z7I+nVCUWc!F{_~Q{-#s1mZ1lNl-W|ILJ&!30c8exf#GZ5a( z73KC70w>n<(vUH1)yQbL1f84Nj{#uYcs6T!53hE%ARmf_8kboC++PvAz*VR+&{&Z$ zxj2x^+3u66w9=^6(qj9|n?f3g-L*Z?ZOzz`&NkdTj7=*kxuAnjlMGk11D$$%J0jmI zA|r=Ui@Su0%~2`KQ$)#!XJQ0=0c9zXW*RhDjkW0S@6(T5f<_h*IqXzTBZHHQLeK1YY=ajiAOv zR;(bf`cW+uz0Ve?ay2V897LQ>f@BHNr6TxDgZ9XgknTk9cbNV#XMq3Jq#S!4I~dKhoGfx|c24jv()mV!u1p)uzh1z56^&-JPuKf(Fad31tI+{p31kpzvvIt?(>l7jo3gB5Q+VS`@W9DGl zFqxTe-M>!r!k(>1(9R|x=+s>rcGkR4v@MD;>I?NNPxHqwFT*MS(TbLcoy-^w_>|9^ z92i|VU#t;3%0NRG*Xo|4!h%<F@pr@} zIjqjO-5ZQb&Jzt&Vg8Hs0n!U_tN(LgDBoRj%~fkepqWp4W4Ur*7&3e~vcJN`J8$}m z2a8M$XjG64N|$Z|Pws9EpDH_*WM6Cr`(4G*C;% z|7P;&3_4min!uAP&_>GDE0TnRppUweC5+A5dIl(%6s?TAWbya*$8tr>C+FcI#)t@>{B=Vn)hmay3@t5YwJGvJ_RMbkzPhohmNA)`t8f>x z_wE}UPYZsUU92@8mSfzGGzT0U9?rHnm>xz-Oy`oHDPxS|<4!E%AnM@iVu*tc2Fg@X z(>>aLKI_voTZ-2F@EcpD68n*qk>1xaMbBgboOQcIzn1kj?3kKojbO+)~5GsT(Zj$RA)Y(G%_N^<9*A{qpN$%T8+hj z(>PT7ey^qwJV|;pB8-rhw$j$NYtRfvsYwq>^iPMs93}s}*1F)v;Pn;%diVGH7Vwd=!4H%{#3F9y+#4IY62VQZM1_~CCXfcj5`%# zN4&e!WpaGH%Jsd(3gmbMdjs>=#f9?k)lPdl$L@~6REV9hwT2nRs6ayWyz84Sh0JTf*&ZH$LH) zpo%0-s*G$p(=F+&=c<(DwH!Gl!*vFMb8~Z}5t(Pn6n%?2L{gU0;`jjxX3I6rT1Zyyh!KJ)hWZowgp z;%2N_8Dgv-_3sR$-1JTLSl66~dc;>Vv6Da-NfPLyU&neOaJ8lcwtX<6dm$@M6zAt0 zsg*6J-rat!j*E+fkkVex6$YO(QA|vBKS5H|DP=l3*jX&(j+-hIAGTl|sE~Gw=H|v&M8}8oEC7-lkkCJ}A>y!o*~a5~9!^Y~DEsEN ztsX`!ssWXZ!&bbVGhXIgH8$3+Eql)2c?f~NhpJiNMfgj912Chdje5P((`Ow(OuHZ3 zy{x0_^z+jeK|9U)d`0 zxBdgJRHe#!@s^Arv)EQeP6XV|BY~WT{d}lTTI{ex~VB`6?=oc*zwzHeQw4>dAyET=vYjJDzq$W-daf0GL=DYIz zK0(1DA!U`7m1CQ(Dk&vjg<*0!I=P44FhaCuD{A;Rx5I(JRxLS+`$r@)ybZ~{gdnx+ zsz9$!C`T+Y_vf+t|EN9RGnVU_hjS}+r zb4*x>r&F(*IkYXVm4?#;5$NhxjcpsYq>EWlINnJ=5TGokGRwT5Hr>P|B@1SCjX4fj zohMysG^!YY@hFFB`AjuV@GK0O1%Za@)!8wmOib}92A&ZmIlekY%V*;s6e=ml$;)RC z(fGU?E;~J7i+9t4ajWrhx)+{k1R^j>WiTxqXdwcWQ77{yUuDe2m0DABJ~APY1rR7{ zVL<@^ZF-JFp^5-75DLpx9A0kChoXNJ)YaD~#*lzHR^aox0+c#skypQqzyR=Q-*y*U zegK?~r_{}&r2IEb1|ul>ht1D}G3lKro8ZC`FHILpS~1D-@kEPS zhx|f){rzC2IBV*$5X54&&Kyn8jqs30yL};b!edVU=d1OS5fKr2CdpP@>USxm!Y~e! zApL`%bg(F2d<>v=GNT>)?Ms<8cI4F1pNF$$S#}K%r1-wm3MBo5N)(X|%&JDjuI22Z z-&U25I7F0+*=nwr8xb*4v;>G5Bih8lE!N8wwM#XIj9@ARnqS3VfZESx)MOnACsdJ* z&N2G+{3WZe0y|WY@VCN_R^MZ#qRs|C7~;#AtzNL{q2A>h^20X#!VO3%8JRI4F2^q+ zYa98)KQ@QIP((IZw>2&RCu&-k-^>OhG52e*MH`ov94TOkFZqSmFz%sS1u%X?NO9L% zEz$^m!SOU*WI^4-^WQJ+RG`zKeWqHM0ZAQMs~{7vIfr{?~4 zqwKF9SO#$o$?4QnHqLA&m&Gob}^pPcgr-{)_)dMwL(&&R86 zMFoXeh>?Oz&nqX7R~eWayPs7y*H$>+FgkEVz8#=qVPlt^uQwy(Vry=kEu<6}!raun zG}2o_ueLatz97s>IutoNIY}!hXrQ5?ak*XM)z;SjT5m!GZ2!3^d}>9>7gMvH#i6o< zc5rY|#>2-i)2PDf>gsa$^xV6=tgfrAO#o0|kdTlhZMM7Bdp=mj5(+A)sGzN`t~Os@ z|FJnJ#g0Qr*73Ozl<$p6kO?_mbBoN+rNY9fha5DKsHO7nfG#8BRj}zpe({D?%S#L6C zNrQ9EjKLIUr$JX5L`QCpOjoM(pKGgmxI7hb8*Bx8&2venP^CVHQ@NArI(d0Cm6nPc9anrUufk(G~1 zU3eOnI8yKKMqBJ-X_ZmQ@VKS=Zbh87@9yTc{?Y|n0!J+?L;2$ad$+GPN_h~z<|VyOY7NWH;~$!2eEuNoR^QE zia8;HPLcRyywN4&zMEVwyDK`hz>w&m(d{6d@dnpaU6?&m!bwye&c2gHTp6xhmCgdA z{>Pw60{oJG5=Vo^QmG3M%hVuxr#ET|=eziAo=-^PvxnzggErHm^};}jqq&~GzJtBJ zP?d(MGDck;ORM7x95$ogj+rwX^yAh3c~Nj6p|1A+dOmyO8^1#1)aLB!n(@S>Fms0a z`gTEOaMZ*Jik!J3w#kzG3-7Z!O@j_1P8Jp$4a!Eb)%_;_b_mz*`Z&igf41}oNDJ2hXEy8Ar95yoka#=1H9ShCc{Zoc z#t_5D7rwvULza6_mWXM=la`GKA5h%E_n%I!qWF|-3111Q;tjkB2~wIyoT^4dPcN@Z zTD5_pRmciB%-PkoygAFisnMx-cjQX8bo480`Kk0@RJiuVDO{%s<@T=hU{CHX79SSz zLTY};=ybXJsRR}okE>^k-%G30k$k<`mU!K0pjXh}!qlLr+sm$!mdA8t>YMnQ%;m{Y zlPb&)>?zdRSK4xQ*X|28oJO3^WF$7LSc!p~&2#n%?I!&C)xLOQ(dXN7#+%1S2327d zif;$^C-X8n0zTo<(S^RbgofR$nKMfdy+$OQ-Oo4A^-gyV)*Mh(jfe%068W=dhnC&l zqL48LaZ6)xrUneu%n4)!0#bdmqGZ`z60a}y@o5gntClV+o|;4Sxma5<>3gexVUthU z2gYVmRec34g7P*DI@oydacF6$uHb;5aodizXA;gr-C*F~IZPnxfgS>WL4NW@9| zRPEbsv2F|%ogs4g$L8eWjL@-g`wT2!qoA=CGAd+Yo_WA-U2dQ*q!3>a!M{-_#BVbF zdAU?NCY}A}_Hk%(64479`reZtD=Ost{MX$4oRn0HnBFhC*#zZGfghqW7FdACMJtpA zAb`E?aiGY8h$aR~RRKxH0~ReAt}~0DJ0)?I_dzxm^oY)5G?J)zyJlUZTmuTaYQ8_j zHoU%AHguxqPV?^y`uVm+xzg5#nItiQFHWC5{Rg&&`Ms7y?$h#K|8r_+E3~0OE<5L$ z_D=JPgo!C$K7(WT@KBvXq)-hyeAF!8>3C}F552B?ha~)wFFz7{yj{y%iR9M%o||uK zb8P(bcF}I>@u5_AN;w8EuUu>EfZe8)*b3j2tq!zML7U6TN+WF^KCeoubp^UPPLF zSrRsaqYwz#;@v1aBsbe!KIhG)6%-V_{Px63P6R*1LdrqT@6K?^x=p?DV{kMc2)O0bUleR^5&pZ{RX@3?1!`EiiWkh&uG;$r1;T9O4_TgHwO7r zHPil$C;Pv(Lq;vllE}IO0tx*IHwaoS5}vu+j`msfC*XE$Uc;bx6}>#23)ExRJ_{q z7IdNM>gp;oL3O*@n!Gmd=kfu4+w6KF1dBsV`aObQ>2>mBs?u=s$}q0LuB76SneR zC;JCpJI7B3 zNjFq=Y8+Z?xz^T7Pg@8{3(&|HvX1Xu`t7dtm{HG<>vr|Fn>=h~DX_RJL9$+M`EK@9 ziEe!~F;L>Q*UK%lncJ;d(}JIwZd=rw__{$sL9n_l;S+a2?YG-{Dy%332_1;$+_q9S z99M$KUT16@SDMDie=AXi#Xw_I{TTuamh|ff=ibH9gpm;`*!Z`@y*&{L4WtxR)>-Z~ z<9#DpXT5Km5`Hp4Tl?I8CvY0C`8g_5?dkFZqjz7Fwa6kDZ?m}_x2cW>A?{iJES1Ul zTmA7Az03G&s}t}~m0m>S;K->%fplDyxV5$Jk%*Wba2oU}Pi(ty$oKhv*YR?cVQ$j8 z-fTDB;;=8p*OD?K?k7N^9zm`Mv<{Pv@ejn^QN_Mex}9+Z&0*r)6DT&%=AhxH_{Rj% z^GNbClvw;uNDhjMih>yVz~QndN?Y~dW8DQV*>Mky6Hergn%X&Vv1Aim;Ow1TM$f7_ zUmfB@7t|yRq>tjyU$}NiM452ZUmgOv3A;kRf*28`9*b`?>twc?IE-}v=k1Da$9pfrmu0Nzz7(vyaRK1ae?S>C$O6x~Y7ANh5;3l! zVPVL~+fLygvd#5R|9&>$AlAfPjy9u9m=rCWaA4yTv0~X8Z`ZOyAbNj`3za_@>1w@> zo|=lSB8;Z=Jo&bj!l#Yhe?yvS};^2gK6>k3U5(>`pLjQKG5`8b69LZ?0e_AkwrUG;8Q?SqKX08 zsUVvZ+pC2E?}e0OGTi(9h<;PyeQ>izg~Ty*-g!fUolms2M+7^U@}+uZ(C z?yNNRK~6Pi;T69t4lZtmNeO$}#J>hB0erz|Q|m61jYNI;=DT!uYc+G@q%9TwX4O0q zNYDF!U_u`{zR%0Iq@1+PE5E}ODtZgmotQ|lva0ZH8&5}+eM}QnOGxkLAe(&O85S$l zjMqLvPrtNv!?f#x`}-%OM-g`MZtRGS#rKaZ>SxYo<+4dQOZOd0eC7KeL=<8iNza0JO$ zvjviA)OmkihXVZl10~A3oA5)uO%p`hPh50<;r)`H961Vc^M!)T9{&R(KZ2RKwCd42lz^IP3Eu!TyMtT54KgC|Mdy4}WoDHrG2GujyvE3!8~2 z7A?Iz9=;j8V?H{{QyDdi;>pV=k61Eu)Irzgb31krnDHI>ez-NryJx8PQL zpb?ShdfXk+`1$#1{1O;RM|`VwbsZSeiAy#Ji)kdl^}_x2t4g}%_Y~zeW8q5=?1X+3 zK44m+*HLWSnYo_$WyM&5L3gAF^zn1P_`GY&ZJ|_I?dE28C;H?7XpyUu(uE7zs3Gg> zi1t?Er??OGaI#pjOzMCagU6rVFtSgEw-U6pCZSO2*J5yH%tGsZ-*1wRoY_OU%}BN@ z!^6?E1i4&^TW!2F6v4ra99 z+XYLvKurgThq3dW1^ekDU*=8WdwspE{>lDC_F`%s%Xfj>6Uig4(~Tl{^ewNr7JA%k zn@uFg!;Q8CYom+6i-lI!$HzxHgN{^KRV=2%W_D*=^|;FmoV_buSZFWZTSSSbtEhwi zvM45;$uBmvvYwJPRcC$_qVm-_wdMEeLx%-#)d<(AAi0Hj_07T6_2_1p5cqt(%VxKQ zm#i~c%AWw4H3fmwnLQD8oVHm8*$<6mq(Oat7ggo=wBwfd`ArAX$h2MCo^g0BG;EOPJcM7Sw~Qju=m zf7LRHSiv^llPC10lLrL@V;};ZQtj>OO4DL8eenI|PFh}`K67eKCa%|Vz(iT8V(}~M zD3{74Y(Vvpd%o4F3kwf#x?HV)aD2SKvm;b*F=@G4M?RU(-akGr>+=5W@bx!M%L0Pg z^{u=ko1y99&PeuEm(f!SVZHe{)>@vCpGycF=o!wUqhoXO|S~Yp)7<6j28j~u95e^NF z{)ieSqg-Xjs*fxC*|eUHTSBDxtuGK9oy>kT5WZr^m@7^XJtjFrKQzruecm}OR*+a(X(yV>pru2j~gX87U5u4+6sO=+=7L*wPX zq3({y@0rwSy^My99X4v#x4fJ(Goz%_M5kW^aXDK-dDq`3#p4?c*QG8a@BhDOIt#Wo zo33jEMT)m*@w-sm-MzTGLveQr4lSj)ySqEVgS)%C2MO-}9eLj4`w5fGH8Xqdwa#tj z=B=uf^Fx?74x=vwmv^Jx>&El>xP%oCxQ@?kxSwX34fSikKJLX7nY^VVA^}Yn=-4K6 z1}3ZG;fK-Z2^l9jKgkGr{0$BLOv%Hejj(-iP;|1^%k9cIMIZ0E_W0z8ug2HO9^YJn?EX??FyNAPeF|v8nJ0i#bIgEha;`06FVy;|+ zo)2)Y|82HNbZ<~W88%D`Bf0!5(!4ICbsd6VT$|zP*san~44EZlxo+U!P*kx}&LpPr zt1{gpbFC&L3>#CX6r&Q~W^a+?uN|SzCEHZ^VR1KTE@jnDlV~x{g%43)a<$3P zXx|MLpXq;jtKi_coPZa@K_V`@T>G^q-@65^B9jf9h4SEL5PwQE$)Fuua=LxBYDFtC zNw8rRIm_j}X&pM5P$tC3?lAzI1qhAud(29xX$G5R3e$@tZ2SWT^c1vED`t8&I4Y7A zC5=o@9xm6JT2n&RWR`x?%;cHac$AT~FtUqmc<^(rZ<(JNru(#BpzW2I10c zCWhZt99}5t`VtR9AgZpcz|@H!62t=&0|?B_HF}*!oxZ$M;nw)HvKAIpP<^MbON5WV z0I{B#nqmUnyduPTp$``aL+e+BiO}7l=yxpgh-vLycchNP8#$QDYC1dyYEJPGe7l{? zebC>$zVG?b7wD{4E}Wv2o+8rm8AC zP+D)eY50rH@KjkFh15*t_sXyC)Qlj%ui`mm{2q`_%YftAq7Z-M*<-h$c|vrHg0B*z z|25aTyAO@l!O6(T6c!f?QNXGqLIqD~Tx|by=Z;OWHiKD7A&$>lV51oqLTTf4& z&iZFp7K*+bAUQE{o`;?Nrn;mV2m~EpiA$)o zxE;sa4F^mgxmi_J4o|kdsc9-IDgrV?Dk|t5jfh+Z7Z%*N`@^tNyDGHncEt_4Zg1Vt z!v91iR0ssShhRy&xi!E`TLU1=tO(tdxyi)S+^Q(y-w8Xi*Vpxs7}>s|rC#j5O~6bE ze+b0I#X;7ZW4!N9HaxkXm9?(C-J>OIm~q8zXz)<^^!VFPILYsC&&b$H-K)qXoZa{~ z6h1nIB;zwOh@yyu*Q4w4nep1|7iLP>!a4HsRp81HmFY-1;$T{X`a)3uiFqWQROn&- zEYdVS-S=IK|I^VS_zdzCEX&9snk!c5TUtsPj3Q#+`N#cD2m;!11y8~v)I@186cvhU zRrNNT+s@XzqVP9wmg;@YLj6$TGuv$U3jWCEbEku;(rxrUs=&v^)o@BV)50AvNuGl= zQO2IA!qM8o?&AG`y;h=&LX^T(V$yFB5fkfG*jlL6!B$e5;-Bmr7&s^nFmh~Zs;leS z+WN?=-dcI&nbtElm?@jrA7?hJG2rhNttc|#3}BEf$TVxF|NW>bQKad*+aCsEI*+06y<6{QtodM7S-RTrt472Yzfx512KWww7ZeG14 zH42eVy(&7Y1oO1Uz9OK_FrP-z-(RRgtA>f#*ch^0$E`so!~g<7`pYaBq0ypb=UN9j zQ}f`Cl7~ChYl^4J8;Y;c9z_rm>Y$iQxuIJ0`r7)4q2n~a-}5cwR@dWh1fX(obZo4^ zajR24mF4$Lfz-{4We%?4G-+`|LxbXsii#)vLaS!51|QtdI_+AIN|7k0jW7YP^4gf$ zs5ZcbVGGHHWR8#@w9+JdZnNKLOS%aQn?G8{t%|BbJ1~qSB0WgLlD5Xh$1j25d49}q zOn%_la$#jBa4uI>CiB_WHA*9Dw3_*2d9iD-0iMj-OHdLr9*iXThK9)Je$pTMm4fu7 zEsMw5MBLj0!!Rm6xh;g6!2qr^Q6NyBzM00;P0Pxbxm%o!v^k zYMr60#RObca^U70tKdEojV!-Z&Y$v4cFRfWz(ma@pM$pfN=hk+eT7=2dZ{Wpk)zRK z&D&|yobSim!^3U(T6edI|I1l(NT-AC2c0(A#dfSJd*X|Gal3#35yGA-h}6|`ipizz zD}`ijz=uDTQnvid{aKk|2cJfTCM6F~DzqVK0P?tcd)P_|cjxnfe7iZE=&oe092q=pe_=o_I^~fC*LoH1yrW140yuc}YdpzbXaWtu;mM^o49W`{(85 zRq8ckI67X0bUL7dr-nLZ-Piw|B)3KA^z5}aHF%zpU@;pVo;7dMxvZ9JR9r(Vf3F)0 z#T+38J39uShjVEDpreAT{A1KIM{&@KYtE_v8%~0NpZgHZ6`d7XWK2v74DA~%7LAmh zO3{~uxIn`<<-p(pGy^uDdpkS^ZIK$o4krLWs$?#2!N%FcquB4|UOq=CD~gC4kC$OI z!1`b`DKj#v=r|RBdSxYPw1N7o?*I1!yni0dB!1Ox9;$NqRIHGtTYT~J;e2E4&lBpU z?H2MB)db7I$k_Cl4|1V=$7~ag`^mhDVkS?|9zB;#C{AmM#*v*pGSND!_`EZ@smbj6 z#zw>$MpkVx5Vo2Y1}yzF7w#LM*BO3G^U?(rEUbgM1Sybp;b zHy{9yNmu<4BL}*#8O6o8GG*i~hOeV|o}Y~;Q%)-Qp{~fvMf5WDGOVz$u$c93WAjdU z6Sg!S=UsTZsDI&0gy92cCu%tArB;{y(yFs_X;-I_{Lw`_5Xh@qm2P9H0m!2aHD(|J zANwn=#2dAw3Hao_rdRwfaA=Cn%yMsuDpB{-zj~`Hlf&^egF}oQEh8^;admk##;w#q zxhRm=a7 z|2p5VQK9!#ZDsWsQB6m9RthG%lu*Q#k!H~=evwneh%^XOc!>AE&>5u|6Yyk~xrJT; zn>7~DVv4Hv;=dEYCyu8y0ydNj!In+@{pUtmxI&y(p+d0%n|GJO&0{g^y4=RP@B0hQ z(lSbxvqNOuOcAG7`w2=|fO}#Rp=UU~W?9NbY~3Qwf>|qN+lErk^gly?`u^mjN}q#{(B&v5>AdAS_)1J~H_g(=fTvdnq#WwS*{aH%4n zW3%m=CNE-QX%+)Z&2HK!M|j(GxIjSmn=~KrCYY=6jax1>3?u<8R>(QQJ%z?qh#cF0 z2RMcni&nahzjBgCr#C*rJbUXX^!j=quqIyL+KNAV=^!#X&>LS$S=iMpf{v|7SFqx*dF6a}!+=nFHh8IQ4 zi2zy%m#rEc8NlWK$WQ!?+}EXP@LCRUe98Eia_n)riqzGc(7bQ$tI&aMQw*`ZXN4wI zzj?kp71mg5vy35SFsl0#pQm8`a0ll`N~#?tdEDlWR_(v?fJx%K@@WV;R;2ZJ_QN?2 zJ5%th)k}b@ws0*{7N3V%-(Zx|Kzp=sy>TC68{qhlRGLde4iJqRU#qfMsy(hkHP6+Z zg7cmVf2n$ZG#Pyzbh(#<@u3je*+22g=QpGthqsD*D!z!dq(pGhazs|9*kuWS+Q$oc z5D;X&Ui)3G^YPDD3dvXSn%7#Bef!P*TBeXsayj#D(a3Vl^bz`sG2_mawmynEvnL)I z*@aUM;}*d9;FkU{5$%f?L~&$1*A1BbS1E1s3Y*%q+JHYkAt7fJCz~I@WWeXqKQfhr z;hMaw+3-j)C8}JuaHOyQ25R7LrdoLu%0HB&L8i_m|iefT;F!XuD{YwY6*xL zdY3=EbsKGP7`v%x-qy@xtLeBWoJi+-nvx9|8W>=8>V{cuu<<4c(-HgIoXC2()5m@TB>dAg`I zmBs&{-Z8{#I(T_K#=K|H$Y-GC9|CAa(Pm>OJ+{#<(kz%c4t+lI^XL%FynS2?I29}! z+AOkN~e81U5w9Un=pt1Q$R#=T9t+NYfT@=YkWTU3~w)YdygXtiMDgucsc%I zz!H2-Qz}e6?)m|nl;h9aE;ov%79a|3RhR~WLg3wizj_e zs&i;SMy|wR!|UyCsq7B3QmDmoOUS}kFbjRlKSJhrFQ+Z`^WjS(itQhhX1Tb2YMM6> z;)sl;0`)T0^%m#8yl2=O_y2iStT)?v2Tp^hjc1zcOWvct%(S*S`&}rKZv{lmpDul4 zGyA+Z81=G5{bjS&HRO1%_-Y`L)@&J%hVuf^$JYD54@9*z1 z3$r|~hiGvi0CuY>55&D}xa1iCSq^x092y#8 zKdY!oeVE{0mys`YtKj1B2fzv8(wUA(2SE6=bS(w9I@S|XH`YGl9;)0I=S;`^+hvhV z*fW5bbdArB3kNPP?&5VitpQKuGi%(g{(FS8`9P$Img7tswI*V7`b27$TD13p@ZG(= z2+2 zJ#7w0CchWV+!s`4<+9m?Do&5>ZdlDK?Vj+^&*#O3%B9{c_QH>m^-}^7vQ>kb$65g+jzN5)Qk{*Hk*`XTcLv88JW@c0NSF}cP{}#&CET>t{ zGX*7HkFnQ6oRhhVpGsjSXemZN&ksY#D(gx%7aH6#nfG<3rGth#J z{Y}^y?#^cEFwNu4YPt6N!k<4hnvj{Mw-ulzuh%a8%l*0e)VV?bD-Np2uZ(++lbM)Z zbiy4orx6GD*C^}^*(y=+j>Kvh84K0s2be2WHwsvSZjO1 zSUU?b@iX#Rr?ByOe`30rrYgA(?MN#7ED-bnH2_CwI*Nb~qc_)xI!d_t000g?Fl4DA zwJvR9d3L-W_>lp-o3wG%0=UQ)_pA0L`On$e8KwpADn;;CMiqyF;J9?&X0*{O+GXb~ zl;1n`##YP8X3HU3KJ#;+XhE^&*{XWAZWI=!+%6a#k^$n^jY5=AVIhqAK}F^AqDjoE zRpY$-=~jt{8u;EWE?1Gbu1-eR5S*Djhz_{{3qrD?@%Y zYK}FTwX=APo!RA?9dz0yE~5y4fcZRau)%`EB$ijsSvpt6{k~wktCz<{N-3M?ylBd9 zZagXdv38+V!2L^6ao48bx7uD??mFt5e&%~?+=)$N_&wu^s{Q6bQx+qBkGZKeeCO6_41gxc(3f3WCd{A$C|+_D+B#jpM99EF2O&&n-1eIy8-jnI*4+` z$flBbG|J0ESyeYu?0zF$R1=%hy>E#_&w7pJh}Sk3PHlgehUUhiDkC9_$*g0MvPJpg zr>h*c%4wKgo5Sm&=s%2Ff4(Vo_E*?^S&T$m(L!-S{dxXdz`*5X^qVj0*rtw1S);D} z!?}yiXh_QuhkVW@d82J&PwIWdP8A(XMwQjPjbXqsZ?@xFY=_k3SFa;taDD(e*Sk>J zv822Oy@!Nofo7iT?Cy?5cULVpt}K3$Vk6sMc&%LTBGr~Q&@ZlZmSUoSqym(|NCy4P ztjF6b2lX2D_^X^6i;s#=Ry^i}!KeTBF{W@4SVYzZVWkxnlVQ9F*E@W;`9*wv%`X^A zVRsAnUdNeR0(J!0g$U%Wf=0j1`FDYaEt*Zohx0>gkUID{^R!B+Qi1Y?h34_;4nG9h zQ9%>Yk`+A@+mWCQSWNCulZar518)U-!pe2V_9JJ+!oO9E62KCREY6+rbmxTuJC9z5C%8$X9LPC!bHqV0L)R%i(7ZLG?$BGe6vuCg&6 zeLPJ74%qaWOb}pH^p0)dk|}Y|+bjen|Mal!bmd1fz)*Fe5#AY?m z{mSxg(A%l=Cm|yPhlU{(b~m2nzvJWR1(b>A1y5H=_F?qfKWopSTV!Fplxad)VlzC} zwQFQ&ijny=+V))Ic=S3{s5(@rR6qpaK=^`1-#a%o9N}D4gPm@cN*^C~z#+$s++n_l zM>2B_%xKvv#ELQ2m*_uXfH7`-2QXKqOu16?Jd01Xj(l^zoG@_V){UvrW^y-y71<{po}&au3_qFtgdO`5MHrj<+T%KuIG0fbvlUzZ(L-Wb5uH9iv$EcQpeTPEcf-6wC)sF?mfL$l=rCg zUkP`EsCKCRnaPqC7Ka+l%*-L$t6W=%(u(-W4I(I18MLT5gpJ5nvytd}GO{|T;S|wFp2H%pV>X@lT8vbnV#I<2IOv$D#r=iU_=6~D zal_!m1a}@UbP*Y*;*-LUi3iwSs|OJiOo<(XrazU=*C`5L5a#J@A_=8N?`+Y097#>F z=Z@yS!$(abL+&9*%D+t-odwT@sHe7mO~s)SKki^@nK?L9ABqPxzAnm3d7!A6_{S}Lz2FI`RP6w*FDyHmubt`AN zW?ND-gl_8{3FG`~T0rNq?|%^6<-;ubWz09>7{Hq;6BY?DPc;XT7Q+M^+ce=zw$l5l zOIKCe!6$x5`HRDaKw%T^3};%E_%>`RXSdtac0uFs82}RMN<;Hk-;KIeoL#JV9-m_> zq!^<^2NSw;(roypO2w`e!@LdGDV0v&r}d?^#|;3C=bMR@=ZcFttzI=l79W6$i7VPs ztHxqu_0E^e5jA3oq)?)duE9n-_#{bIb$N}XN>f4C0*`uXYyacW{vaeF<@3o#iVcNS zfp!(!--oa3Le6N7lHzfMjl{b*`|?z7g?a6ClsV_qbV;(X-@Lz1?qCqxCdV}ju9^b1 ztvPHMih9#WR`!*wZV@A|nnQIu5>G)MvHsMgNhVTt^rf7HvPpC@Q758*IZ*Oc3M3P2 zly?9|U?@}MW=hy{IQIK^D(jxo$6@xc^gm3Z$*MJg6P&=UF3OYlZlxO1HaiAKT?eJ6e)^=#AvYs|IFB)V%A+aom`>3R3fUvK&3*m1VbbGJmpen`RHdIe25L60 z(=AhLcE~oOpce7&jxVpbsaPW~-3Fpx6e1|2^#+gzrI<9qcKQCpFvp^Ice-$Rz4ZiFf z|LBAK8d%9zj{L=*q5NqwMp;PE@?UL9iDh>7#oj5Z2T^jQo4Sq0qzQTb)5mRvN=uz^ zyn6?KT{VE(0TKf*(X%oaaet$!+~m%`B8m0iZrDF*k^i#D7k6~%U;>}-PEBQH4v(}F zbY|;?q)HS965|<&PPC}$hO5^zw0IZL*Kc@u7lwwAU|c2B6RDq2Jt~suGnRnmuC`p5 z=Y3~;QphMMqnn!patCF*`1nIrKt;JFwHDbgQqjcU z|NAe{<80MfR$(WEz^Jy+{1ZmN6)gcWz|6F4-YV;kCUC@T^hS7$fUtV$^ac-tsY|yw zF0W34HUlC{LZQ@yVm}iSB_ZyTrKf9}c}{B+zn7wMF)efu>zQ$nFX5Yd(yk zq9VpknpwQ4(5~umOtjo$t?54f`ZC!_6cKl-kRKnv2jth@U{q$RV3ZoFg-WM^l+Aqr z!WK#riqm$o*bcZT=4R1q#G%pZre!+;6e4w>WUh&)RC%2rBpun=pGJAp+U|_LSP7xO zu$V|!sWXwVm`Fzu`4!=$tC&rNpi+czeQ|w#-G+!W{2oA!ueCa3uut3N(4>?aMUb9%cDp9nWvIDyfR3G>>A8L@3ndGC3{%?L_5 z@_#*Z*z?}U=p0TfwTuJ7VGFoc*$>$q zJnZq)o~?%!9)FFGH+>vs-+mvW@_;8$d-PPE@l2ZhPlt<+O0 zeK3I!y(a(mcEvyc+6+yIKviaGL%uNT9PXhdt1a{rY{DQKj(Uru@!%Kqx&Oh&7T@9X zP`>GX%9zyi>(_SoZ`q}4gRqE*0=Jt*H@%L=?#&cXiwC7gMbh-NvKxka7xNaP^i;N> zMi3&pSl!C9kPC}*NEdVM_g%=jXoj_`wZJ`ggZHNhJjQ&{AjHwbi45GpY6DO?m&1B# zo7*uKg;aE*Viw=JAmHB{wiK||nPu&RJ2Z7;%~dLlrysX|K&LK7I=#SJ)9pn1V}My#K$>>sNGoB)r+HQdRX<8JpqTMTg2yz71H?J>1b(b z%Zubv%F{V*i{YSM@Y#BEIrI0|Co4BT!VdoIra5o{eXi}pI>vH!XBl*utID3?fk<9RECK5ts6=pJu#Y4)9&j^;RwDicJC(< z486_;G11V3PERX8-=E0^A)@=Uul^cMq#gCS=m=S{8?106(DmaA>ecvz0=cqMl-pZ7 zx0mVkkMe(eu5;QE3n8pdx%dR13%J-Eoh_1^X$9S^XPRTEy+75}blPhwPUH;K9p@Oc zXOtO$xS)TNR*hlTir3!WUQ%f2=O(+A-fTf17qSN$0+E$Pd)m2jMu~DH@R)JAuBD}_ zgTCc4P~pmL@oTFk=0b&5z!7f(mBMZoV6$grBI~@Oc^h(|(9fPnb6n;N@*p`|YyPm8 zrlxEp3<3e;PQdR`Le;`}d>rrI!11v@pX%lJH4grAu1ZpL^2z+KLI=b+^p%}2x`JquF zTh*FMle^w)(CJ}?&JrItV6w(=GUvqMF`5bwrF?bc&t|Jyn|%hRS=~MR zuDzw;Jz#U+pB5>F=`1479NEWMcgz3sFsTV6}G90=O=0dJlfE7URJs zm<){f%l9Bj3yOTnIUf(fAW>bcHjqF2C3p@_xEqUsHP+1o|nGd@zsXL7mq6I>&--g`N62JFXsj@ z?etm&lXBu^m-?3THk@1$X^>#on<0&>vxK3RI_CI{7l7->^+1UKR+$<#pZm$r6ea`p z01X$@v1CT*DqZ!earr$cZnLgkBQ%3v0*}VKl<`M!RIRF2+98Z2~TKg z+1vH_Z+1fWd$;EYDN>1VcTtqBh{|USjKiW7LzAQFynt3yGGTvxC~8(kMTMBh>Awf( zC`GBdw(p53_OfCBqOKT?uFB3%W?S3hye^pPLNH_FzQh1SsNXql_tf&(G))g3b-(C# zkY)@(pOh{2cl_s>!Vv+==yXk|Kd+wAXS_U3Mx>|kxu@nFhraqe`uOwJrethg_*m^5 zh={-#8yh#+E=5mf@mE~4iP7-!=@M`|5RWA@j$zER9az1-54rT*cL#IQFW0wxm2Z! zV~2N>GfA#hwqSLKs9aV{$Ueh&3jE+7dsIgXxq25giSbohj|=K2i6%cdj0*85!^4x* z*8XO{)>NcYAn7I4T2xrbVlz*3km1-lg1*8h{w_7~UX$yQ`>Yr+bP#2`Ea?3sd?L9d zxf6a^&1O1RbTpIKRiub~{|z>O7bu;M6}Qu0oj5t(-#}B5vrAj;^7O`ApuS#?PHp; z<&7WRfHXJ8^hdR!Fk!FAN3|iDHoUiK^f%Qa*{_6zvNwkl$e5TVKf|zM+*IY{<%v10 zXA2!QE46zjGPqUB7StirN#1>1{Z&@ZPZ%z`7J6N#V7;3BWji!Nj!?(Wr@oMe>)!X`j~=VcZ*pKUf*6GuyJuE#%oTLd0X353cXq0Ic-%+WxoW|X$%znquO~&2j7x{#D3~FbUjh{}xMDBX;56T}kE9LWK zHLe(qkS8b@;15C}T=Zqa8$Yf_CC&{5raEpcTYYwhLTnTcO4ZA-zJLGDVKW~Z8~cS$ zqx?3*OE|{lU^Hp8-h9j@WWRjg1}?p+a8a&imrP#@Bh`qP)rT;oD-%9kHMh4w1pae( zjPQG@8)P}ILaSz1hc3{vc!GJlX!0YJS1^iQ?$FiZinu;L0mf>!5E*)=_lJHBc*&Jk zP&gQlqZr-k3*mL%g)dRcc~_uE5UHBk)oDZP#HHIM_!6phT<@906l5v#pEt8($m*fa zh)m(TThRP+j&z$xaQ~6q4}&PGr{{&OtkfegSjx$X#r6B*odP4;VW%{21AkzKlV z(ME$#t6Dx71y_AisfFkw7~fD?vs(XY4KF7FTcUg^->hIT5j_C!g6+Tz{DCdAl)u7f z2Q$K|HEyk1Dq^{dH3XAK^s^f z2#J4IQBnCg`JIrMIL}5!Wnn2Vk5*S#x8C9$wASp1jgK!SCI;8+ut9!Ct7A1jFc2L7 zzh0QsNIWI2YT;)}*~I@?SXjVPNkE2kVf&sx2%~|ppPcS*d)ae2vWwuZP(Bw*-~9?- zEMrFHKbSeVSgdz^<=19opXTR}Gq2x1JDb2a@)hm!e=9`Toz}vTkzpK)CIPuV-yXxX zfC#0>Sau;hGC<4{)nW}jUa|M`U9Tz!mCV;1!)ZhdkiMzX^#g>JII=iS`UvR#_gelL z=Go1r_eGrl=8!sKhP9isI_B9OX5pa6Z4l=N7RA`#_{6Uzn(rXqW z#zrvFTXC7+rbNhVAh z*qKFmbCBxq=>swyAND@-!ml1=z1|yu2z$UbO3Gc1iI?%;y3@{F(jVS(be?TS@26JG zJ`aaLfj{Oi_h*R?i{VX1%)=?>&mZVlCDA~~^|jUksuzIu=+YkI>&^{&wRe2{?BkYH zgm$}wFwEXijOBWZv+C#%BSXREGc+OQK75idTvDnHRkN zp=C_!%Ly2N(vVYucn~6LZFgw#&ovk(b-gz8Q8N-!Gm=ec_qBZJMNL>(9!=-TXKFMT#XjEOk|ZMVcJi0%tgb8E?8(Au1+hVIHNuYbnD}Wte&|~{EzoAT?1LeP zrys}UIVr;PHXjjdPHD{M(9%*0HnpO1xjHR!tgqErGNUZW<4oC_qag>RUw#xXNPGEO zUPPS5#o1gr)$K5mdjDR)C|aRe;pE}EvlfMP^l-mqW|4NhR6~Y}$DmwBT-ykCf;9oI zeB*J-t+$#X_ibO-7I@NFZLAJq<=l%ivVBBCO>O4$X{nJj7AB1JO_3!5;SO5_^rop* zo^5;u4fKvC(SK-$VP9<#33%2*4VY-1!AG4I{M8b#s16m7OIQrW^x=+(l_HN-j}R;-w}bVN)ot_vR7t>!K&R^BY=fC9%&fuzBn!%SZNK>17_73c#N(UK(NFf!jO zA^n>g8|lm4q}0&$`$v_@X(vSEEquOIV)+R3vceF!&2g$0ud~3L*`Qa4M$8l2>I#OTe4dx50`!_*8&cW2=r=NyCJF9-aKok5D=1rKS z@N0YiFV}-sLpgz^G(;0Kg~R_*Q_aBa09{FRXV_b(IotQ}+c=Nz}(?cf9xy-@J-I#M-9lfdPs=5TMU za{PL|bl^FCPctV~58Rwfa!yELy*lMgq*3{E1D>i_-`F5A0rJTQ`aU0*jDjvYt z!*p4{CguME3bxy1_E7pr@S6U+a(t^=e*C%l;X+B@Y<3Z<$W#q~ccOIJ{T)NY_k6KV z-0tW@AbdjeSHu7={9K8W);~K)j&fsyD*eY(VfjgpMYSN(e5s~w%yI!c}6+sl8^zV~G)UL0OpRaJFNHi_;` zURrt^~3?toxgwKcfTiWxCea7(CIqW_K0n1Q#lJ3 znm^+HoYL$6K5~qa|10_F`ykOIOY|ee*(AS8Fo}M-?;#AYwVnkqe z&k=U}Fu%l0n)A??P|KG*x;q0QlI-SbpfUDNbJHAPSHkpb0f@wbMN9!%L zc0q$Wq0-vtWi4VR@7Y;)FI!c3xkafsYYuZGXC8jFGBtqBtsg;6cl}dMqa(1+x%;M{ zMDS;cVwN_@qv~%+5OBq=!EQx$1q5J*N=kQN_M5E`WgG_*;oc_=xz;v6wR7)>fe8E| zGdAaoO+lE^#M`|Cu->K3h12?mzW72JVxE0Tb+J4Z_Wo_fO;P1>&PNQ$?h|J{DL^7bRt!*yEh0Kb@V8V zgz9}uLvP$0dNE=JDyElC=38;D9Otarj>#9~b5{8_3+3}QMr0Wy{jIoi+Kbi58Hf)f zhV1{5A>7SatA+7xfA)w-I@9l}-E=FbQurY77KisZ4zp~uO zB@4}k-ubmTJ%JjR$1V6gAj8jhr(sc18U`Q_g+{v-wv(lrsm2^EPr!)_;lsD7P6{xK zXG{K!zl`>KTH)3S`?!QIkoG} zkV}+uG)aVfwcYO_I*1s=pTm16-r{^8RF#q&?Z-!2jZE7EF3L8Op~gMmdkz?jiFAoP zvEa6`)x`h~K?EY*i#P=!iuTa`L$E@a?<_A9jq$)zPDPTW*+g z`Qpse;a+sC5?E#>hoU1bx9`z-Hyh!aH)cYgv2*hmtBiPycjx_C?9|7b9sKlK)rprh z-K))xHBcH|;%DT&R$jS}1Py-Nj3|CfC1=-wAKaLbgY#B)n){rX4aF?JnMPZ!?11;D zq3oRwuNw>VxeK>(`#e zD!r8spNA>q-D8R4Vn$@UuZm^079Y>)Kahk-$I>WoN;N9*d>+o-Q=LzCuhv^#rI1lk z_ePWG=LtD75h=EwwB8-OU+qmWC7ju9Foj9hV`wR#^0ojMn_1B${Ig|h)JcXl0ptPf zTwI#RGX+RU7{uKR+vB&+pFFRg@KV|9Ygfq5Oy;)BEkAZ#8@#V5?6~;X4!h*WrU_fY zh3Nb-v(Cfny?uQvP4*e^!*knNfK3%Dh4g;8jdXU)YvpkQNl2;H{F|R*X3S2OD9NLl z+m=*yNYR4Lv=wLMn+qk`gf>!l=vRtV0k3Opoto#}fr!!TgRzya0AZhp^EciGH@?@P zQ{{SNQ`x_NRa#`Hg}o6x-Cr!4J{-Tm48@R@XjbXYK7aWsAwe5O#EqB)eJtm?0X3Cg z9ib}28g;buO9(|jiJB)y8?43m2BSXebXBPm!kV}I^`1^DYGA&x zH(+CbJP2@AoW)7m5(?;hJ?stm^7(vfEQh~-E5}qW$aUqsyk28Z1@Xm0S@*{!IkJHQ z=+M$6jV6u_#2X~*6!)t+uIAxUPDE|fe8E$t%so)p=W8Yi;jmt2&agJAJ1p%#&m`R z-l>|M{X$Ag#)NH*g2&@~zoscNg;Tu7);l#g&b63b2NOD}7*h984O9|x5r_wWm0q)^ z$HnGE#-|+9QOmxgxra~WT%?|riy=J=-Hk-e1RNGzAk4SWj0_@Zv{x>L>0=Z>J1I%R z%&f59c8MuJKmTzjBt!~2b45Tlu~7NI=25fqk7MwtX9{DCRG4aMqgPjq$pl+{la$Z7 zfq+ttVMl0aXzb;et{I@E5_A#=PPu;6Z#ZZ_-v)G{(lpe}jWn%2bO!e982I7E#x`5zO(~*@_XdHqevL94=gFcH+*cNzA($ z(|lz2!Rs{YSU&54gi*KQ*HAPGMxA76Un1LxwZmyv);Io2RQBYvA41>5D7O56IxUFQ z*S|lbh*fFsKy0>OD8R>e&sys(?lFOZFy!RqpV86ZraU0~`}=Gmm}H&aN7LUsKi*$L z&a(t=-y3z;Ba*s1JZ08jBX&=~F>-mmEjUbnPe>3>GXq+?Sg7PD^Pw}d(WhbP@C zL~4A&nVGtFLE1m={J>LPbP~SAGPRO&G(_>vCx`_wErZ*Uj+K?Qe_-G%KE9N%uRyco z7L|}tc4Q=)s;a8Aq9T=`U{-A{i^XIn?audWV)DKaOlms1@O+5~D8WKR1cp*B`3DP2 ztjocuO0_<(Y%;?!3o121l;o!GjRKTz3gc#@kup-?tyZm%%#fxN;2E^BeK0xxl<|$r zS?K(n6gq!^RL~nbGjGiMzo(w-gR!J+L7&~@^WNq;!xHlw-DS4=dT?N=NX1O<;%Q4t zY=2}C|D*Q^7Z#Qpt!@KN0<}^x;9}E76;}-A%h#`Y?Qi#M>z#f?!ep3+XE$i#K8?k( zIo$UQ?3x+qiICR+2(<)3Wb7&LL`}Sxj7K|N&{^hZ?w=^Rn8}ofB%fVjU z2{2;{T$5?Z89nTvPHVM8n&IAsJ(|w@WaN8=@cZ{~EK;%1kpBLD8Vadsv%Y`mFo~#_ zmeNy;4Yq`lIWjQ{u#98a%tV)Upt@k z@7+8(XqQXkQm2H#qqh&6e*XwEMXdYi!tJfj+Z~F)S2S6d>ped|UtL?fT5VT)|JHYNWM!UBdoe#b+d01a3jO4>Xa0N7pH#5uJ{m Date: Tue, 26 Apr 2016 14:48:06 -0500 Subject: [PATCH 03/25] Some website contented added :) --- docs/GettingStarted.md | 140 +++++++++++++++++++++++++++++++++++++++++ docs/index.md | 34 +++++++++- mkdocs.yml | 14 +++-- 3 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 docs/GettingStarted.md diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 0000000..e38b869 --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,140 @@ +# Getting Started + +This page is dedicated to helping you get started on your way to making the +next great Discord bot or client with DiscordGo. Once you've done that please +don't forget to submit it to the +[Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) list :). + + +**First, lets cover a few topics so you can make the best choices on how to +move forward from here.** + + +### Master vs Develop +**When installing DiscordGo you will need to decide if you want to use the current +master branch or the bleeding edge development branch.** + +* The **master** branch represents the latest released version of DiscordGo. This +branch will always have a stable and tested version of the library. Each +release is tagged and you can easily download a specific release and view the +release notes on the github [releases](https://github.com/bwmarrin/discordgo/releases) +page. + +* The **develop** branch is where all development happens and almost always has +new features over the master branch. However breaking changes are frequently +added the develop branch and sometimes bugs are introduced. Bugs get fixed +and the breaking changes get documented before pushing to master. + +*So, what should you use?* + +Due to the how frequently the Discord API is changing there is a high chance +that the *master* branch may be lacking important features. Because of that, if +you can accept the constant changing nature of the *develop* branch and the +chance that it may occasionally contain bugs then it is the recommended branch +to use. Otherwise, if you want to tail behind development slightly and have a +more stable package with documented releases then please use the *master* +branch instead. + + +### Client vs Bot + +You probably already know the answer to this but now is a good time to decide +if your goal is to write a client application or a bot. DiscordGo aims to fully +support both client applications and bots but there are some differences +between the two that you should understand. + +#### Client Application +A client application is a program that is intended to be used by a normal user +as a replacement for the official clients that Discord provides. An example of +this would be a terminal client used to read and send messages with your normal +user account or possibly a new desktop client that provides a different set of +features than the official desktop client that Discord already provides. + +Client applications work with normal user accounts and you can login with an +email address and password or a special authentication token. However, normal +user accounts are not allowed to perform any type of automation and doing so can +cause the account to be banned from Discord. Also normal user accounts do not +support multi-server voice connections and some other features that are +exclusive to Bot accounts only. + +To create a new user account (if you have not done so already) visit the +[Discord](https://discordapp.com/) website and click on the +**Try Discord Now, It's Free** button then follow the steps to setup your +new account. + + +### Bot Application +A bot application is a special program that interacts with the Discord servers +to perform some form of automation or provide some type of service. Examples +are things like number trivia games, music streaming, channel moderation, +sending reminders, playing loud airhorn sounds, comic generators, YouTube +integration, Twitch integration.. You're *almost* only limited by your imagination. + +Bot applications require the use of a special Bot account. These accounts are +tied to your personal user account. Bot accounts cannot login with the normal +user clients and they cannot join servers the same way a user does. They do not +have access to some user client specific features however they gain access to +many Bot specific features. + +To create a new bot account first create yourself a normal user account on +Discord then visit the [My Applications](https://discordapp.com/developers/applications/me) +page and click on the **New Application** box. Follow the prompts from there +to finish creating your account. + + +# Requirements + +DiscordGo requires Go version 1.4 or higher. It has been tested to compile and +run successfully on Debian Linux 8, FreeBSD 10, and Windows 7. It is expected +that it should work anywhere Go 1.4 or higher works. If you run into problems +please let us know :) + +You must already have a working Go environment setup to use DiscordGo. If you +are new to Go and have not yet installed and tested it on your computer then +please visit [this page](https://golang.org/doc/install) first then I highly +recommend you walk though [A Tour of Go](https://tour.golang.org/welcome/1) to +help get your familiar with the Go language. Also checkout the relevent Go plugin +for your editor - they are hugely helpful when developing Go code. + +* Vim - [vim-go](https://github.com/fatih/vim-go) +* Sublime - [GoSublime](https://github.com/DisposaBoy/GoSublime) +* Atom - [go-plus](https://atom.io/packages/go-plus) +* Visual Studio - [vscode-go](https://github.com/Microsoft/vscode-go) + + +# Install DiscordGo + +Like any other Go package the fist step is to `go get` the package. This will +always pull the latest released version from the master branch. Then run +`go install` to compile and install the libraries on your system. + +### Linux/BSD + +Run go get to download the package to your GOPATH/src folder. + +```sh +go get github.com/bwmarrin/discordgo +``` + +If you want to use the develop branch, follow these steps next. + +```sh +cd $GOPATH/src/github.com/bwmarrin/discordgo +git checkout develop +``` + +Finally, compile and install the package into the GOPATH/pkg folder. This isn't +absolutely required but doing this will allow the Go plugin for your editor to +provide autocomplete for all DiscordGo functions. + +```sh +cd $GOPATH/src/github.com/bwmarrin/discordgo +go install +``` + +### Windows +Placeholder. + + +# Next... +More coming soon. diff --git a/docs/index.md b/docs/index.md index 3daaca4..9b433da 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,33 @@ -[DiscordGo](img/discordgo.png) +## DiscordGo +
+ -# Discordgo Documentation +[Go](https://golang.org/) (golang) interface for the [Discord](https://discordapp.com/) +chat service. Provides both low-level direct bindings to the +Discord API and helper functions that allow you to make custom clients and chat +bot applications easily. -Placeholder +[Discord](https://discordapp.com/) is an all-in-one voice and text chat for +gamers that's free, secure, and works on both your desktop and phone. + +### Why DiscordGo? +* High Performance +* Minimal Memory & CPU Load +* Low-level bindings to Discord REST API Endpoints +* Support for the data websocket interface +* Multi-Server voice connections (send and receive) +* State tracking and caching + +### Learn More +* Check out the [Getting Started](GettingStarted) section +* Read the reference docs on [Godoc](https://godoc.org/github.com/bwmarrin/discordgo) or [GoWalker](https://gowalker.org/github.com/bwmarrin/discordgo) +* Try the [examples](https://github.com/bwmarrin/discordgo/tree/master/examples) +* Explore [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) + +### Join Us! +Both of the below links take you to chat channels where you can get more +information and support for DiscordGo. There's also a chance to make some +friends :) + +* Join the [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) chat server dedicated to Go programming. +* Join the [Discord API](https://discord.gg/0SBTUU1wZTWT6sqd) chat server dedicated to the Discord API. diff --git a/mkdocs.yml b/mkdocs.yml index 298334d..3ee8eb3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,13 +1,17 @@ site_name: DiscordGo -site_description: Documentation for DiscordGo, A Discord API Library for Golang. site_author: Bruce Marriner site_url: http://bwmarrin.github.io/discordgo/ repo_url: https://github.com/bwmarrin/discordgo -extra_javascript: [] -extra_css: [] +dev_addr: 0.0.0.0:8000 +theme: yeti -theme: readthedocs +markdown_extensions: + - smarty + - toc: + permalink: True + - sane_lists pages: - - ['index.md', 'Home'] + - 'Home': 'index.md' + - 'Getting Started': 'GettingStarted.md' From 58d143ccb39351621b8c1c0742ad9dc9f36cc3fd Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 26 Apr 2016 15:29:35 -0500 Subject: [PATCH 04/25] Slight update.. --- docs/GettingStarted.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index e38b869..efeb3ea 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -63,7 +63,7 @@ To create a new user account (if you have not done so already) visit the new account. -### Bot Application +#### Bot Application A bot application is a special program that interacts with the Discord servers to perform some form of automation or provide some type of service. Examples are things like number trivia games, music streaming, channel moderation, @@ -82,6 +82,8 @@ page and click on the **New Application** box. Follow the prompts from there to finish creating your account. +**More information about Bots vs Client accounts can be found [here](https://discordapp.com/developers/docs/topics/oauth2#bot-vs-user-accounts)** + # Requirements DiscordGo requires Go version 1.4 or higher. It has been tested to compile and @@ -108,7 +110,7 @@ Like any other Go package the fist step is to `go get` the package. This will always pull the latest released version from the master branch. Then run `go install` to compile and install the libraries on your system. -### Linux/BSD +#### Linux/BSD Run go get to download the package to your GOPATH/src folder. @@ -132,7 +134,7 @@ cd $GOPATH/src/github.com/bwmarrin/discordgo go install ``` -### Windows +#### Windows Placeholder. From de235a04f03a596a39eba59f4433a74c4d595f6d Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 26 Apr 2016 20:19:29 -0500 Subject: [PATCH 05/25] Adjust error code returned from UserAvatar test --- restapi_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi_test.go b/restapi_test.go index c35c457..e72a108 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -31,7 +31,7 @@ func TestUserAvatar(t *testing.T) { a, err := dg.UserAvatar("@me") if err != nil { - if err.Error() == `HTTP 404 NOT FOUND, {"message": ""}` { + if err.Error() == `HTTP 404 NOT FOUND, {"message": "404: Not Found"}` { t.Skip("Skipped, @me doesn't have an Avatar") } t.Errorf(err.Error()) From fc7ce3db94cf36c206754b5e7572af8a314b3cf9 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 26 Apr 2016 21:17:51 -0500 Subject: [PATCH 06/25] Initial add of new logging system This adds a LogLevel setting for both Websocket and VoiceConnections that can be configured to set the specific log level desired. This also adds a new logging function that adds additional helpful information when printing out log messages. --- logging.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ structs.go | 3 +- util.go | 35 ----------------------- voice.go | 3 +- 4 files changed, 86 insertions(+), 37 deletions(-) create mode 100644 logging.go delete mode 100644 util.go diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..ba70ae8 --- /dev/null +++ b/logging.go @@ -0,0 +1,82 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to discordgo package logging + +package discordgo + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "runtime" + "strings" +) + +const ( + LogError int = iota + LogWarning + LogNotice + LogDebug +) + +// TODO: Merge util.go code into here + +// dumps debug information to stdout +func msglog(cfgL, msgL int, format string, a ...interface{}) { + + if msgL > cfgL { + return + } + + pc, file, line, _ := runtime.Caller(1) + + files := strings.Split(file, "/") + file = files[len(files)-1] + + name := runtime.FuncForPC(pc).Name() + fns := strings.Split(name, ".") + name = fns[len(fns)-1] + + msg := fmt.Sprintf(format, a...) + + log.Printf("%s:%d:%s %s\n", file, line, name, msg) +} + +func (s *Session) log(msgL int, format string, a ...interface{}) { + + if s.Debug { // Deprecated + s.LogLevel = LogDebug + } + msglog(s.LogLevel, msgL, format, a...) +} + +func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { + + if v.Debug { // Deprecated + v.LogLevel = LogDebug + } + + msglog(v.LogLevel, msgL, format, a...) +} + +// printEvent prints out a WSAPI event. +func printEvent(e *Event) { + log.Println(fmt.Sprintf("Event. Type: %s, State: %d Operation: %d Direction: %d", e.Type, e.State, e.Operation, e.Direction)) + printJSON(e.RawData) +} + +// printJSON is a helper function to display JSON data in a easy to read format. +func printJSON(body []byte) { + var prettyJSON bytes.Buffer + error := json.Indent(&prettyJSON, body, "", "\t") + if error != nil { + log.Print("JSON parse error: ", error) + } + log.Println(string(prettyJSON.Bytes())) +} diff --git a/structs.go b/structs.go index 5e46068..18e24f5 100644 --- a/structs.go +++ b/structs.go @@ -30,7 +30,8 @@ type Session struct { Token string // Debug for printing JSON request/responses - Debug bool + Debug bool // Deprecated, will be removed. + LogLevel int // Should the session reconnect the websocket on errors. ShouldReconnectOnError bool diff --git a/util.go b/util.go deleted file mode 100644 index 935103b..0000000 --- a/util.go +++ /dev/null @@ -1,35 +0,0 @@ -// Discordgo - Discord bindings for Go -// Available at https://github.com/bwmarrin/discordgo - -// Copyright 2015-2016 Bruce Marriner . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This file contains utility functions for the discordgo package. These -// functions are not exported and are likely to change substantially in -// the future to match specific needs of the discordgo package itself. - -package discordgo - -import ( - "bytes" - "encoding/json" - "fmt" - "log" -) - -// printEvent prints out a WSAPI event. -func printEvent(e *Event) { - log.Println(fmt.Sprintf("Event. Type: %s, State: %d Operation: %d Direction: %d", e.Type, e.State, e.Operation, e.Direction)) - printJSON(e.RawData) -} - -// printJSON is a helper function to display JSON data in a easy to read format. -func printJSON(body []byte) { - var prettyJSON bytes.Buffer - error := json.Indent(&prettyJSON, body, "", "\t") - if error != nil { - log.Print("JSON parse error: ", error) - } - log.Println(string(prettyJSON.Bytes())) -} diff --git a/voice.go b/voice.go index c0971d2..b855aa5 100644 --- a/voice.go +++ b/voice.go @@ -32,7 +32,8 @@ import ( type VoiceConnection struct { sync.RWMutex - Debug bool // If true, print extra logging + Debug bool // If true, print extra logging -- DEPRECATED + LogLevel int Ready bool // If true, voice is ready to send/receive audio UserID string GuildID string From 2695ae1b21d07e6a5ad7d901918d4ff61dd95c41 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 26 Apr 2016 21:22:28 -0500 Subject: [PATCH 07/25] clean up comments a little --- logging.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logging.go b/logging.go index ba70ae8..95ba1bb 100644 --- a/logging.go +++ b/logging.go @@ -25,9 +25,7 @@ const ( LogDebug ) -// TODO: Merge util.go code into here - -// dumps debug information to stdout +// logs messages to stderr func msglog(cfgL, msgL int, format string, a ...interface{}) { if msgL > cfgL { @@ -48,6 +46,7 @@ func msglog(cfgL, msgL int, format string, a ...interface{}) { log.Printf("%s:%d:%s %s\n", file, line, name, msg) } +// helper function that wraps msglog for the Session struct func (s *Session) log(msgL int, format string, a ...interface{}) { if s.Debug { // Deprecated @@ -56,6 +55,7 @@ func (s *Session) log(msgL int, format string, a ...interface{}) { msglog(s.LogLevel, msgL, format, a...) } +// helper function that wraps msglog for the VoiceConnection struct func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { if v.Debug { // Deprecated From 852a968873e4b522ccffa3e9e986460c8e7fdb4c Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 26 Apr 2016 21:34:01 -0500 Subject: [PATCH 08/25] Comment log levels. --- logging.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/logging.go b/logging.go index 95ba1bb..639df5f 100644 --- a/logging.go +++ b/logging.go @@ -19,9 +19,17 @@ import ( ) const ( + + // Logs critical errors that can lead to data loss or panic LogError int = iota + + // Logs very abnormal events LogWarning - LogNotice + + // Logs normal basic activity like connect/disconnects + LogInformational + + // Logs detailed activity including all HTTP/Websocket packets. LogDebug ) From 1a4bb2a0041c47025930531ebd81e4345a7c15a0 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 26 Apr 2016 22:00:51 -0500 Subject: [PATCH 09/25] Even more detailed comments on log levels --- logging.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/logging.go b/logging.go index 639df5f..f9d6e45 100644 --- a/logging.go +++ b/logging.go @@ -21,12 +21,15 @@ import ( const ( // Logs critical errors that can lead to data loss or panic + // also, only logs errors that would never be returned to + // a calling function. Such as errors within goroutines. LogError int = iota - // Logs very abnormal events + // Logs very abnormal events even if they're also returend as + // an error to the calling code. LogWarning - // Logs normal basic activity like connect/disconnects + // Logs normal non-error activity like connect/disconnects LogInformational // Logs detailed activity including all HTTP/Websocket packets. From 84f0b5d41adc4ee9b27548e729ee68c71fd20387 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Wed, 27 Apr 2016 14:38:29 -0700 Subject: [PATCH 10/25] Add missing RLock in State.Channel --- state.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/state.go b/state.go index 7993151..b170ce0 100644 --- a/state.go +++ b/state.go @@ -324,6 +324,9 @@ func (s *State) Channel(channelID string) (*Channel, error) { if s == nil { return nil, ErrNilState } + + s.Rlock() + defer s.RUnlock() if c, ok := s.channelMap[channelID]; ok { return c, nil From 7efe4ccfeeb2312d2f4a83f077b2e3d0e7336c19 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Wed, 27 Apr 2016 14:43:43 -0700 Subject: [PATCH 11/25] :ok_hand: --- state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/state.go b/state.go index b170ce0..e3c8908 100644 --- a/state.go +++ b/state.go @@ -325,7 +325,7 @@ func (s *State) Channel(channelID string) (*Channel, error) { return nil, ErrNilState } - s.Rlock() + s.RLock() defer s.RUnlock() if c, ok := s.channelMap[channelID]; ok { From 250579eb3a058174d0dd815967e4319e284f7357 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 07:43:31 -0500 Subject: [PATCH 12/25] Fix incorrect handling of VOICE_SERVER_UPDATE With this change, discordgo now properly supports VOICE_SERVER_UPDATE events and will upon request close existing connections and then re-open the connection to the new voice endpoint. This allows voice gateway redirects and voice region changes to work even while a client is sending audio. --- wsapi.go | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/wsapi.go b/wsapi.go index d16c18f..1c6abba 100644 --- a/wsapi.go +++ b/wsapi.go @@ -363,13 +363,11 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi // Create a new voice session // TODO review what all these things are for.... voice = &VoiceConnection{ - GuildID: gID, - ChannelID: cID, - deaf: deaf, - mute: mute, - session: s, - connected: make(chan bool), - sessionRecv: make(chan string), + GuildID: gID, + ChannelID: cID, + deaf: deaf, + mute: mute, + session: s, } // Store voice in VoiceConnections map for this GuildID @@ -425,9 +423,6 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { // Store the SessionID for later use. voice.UserID = self.ID // TODO: Review voice.sessionID = st.SessionID - - // TODO: Consider this... - // voice.sessionRecv <- st.SessionID } // onVoiceServerUpdate handles the Voice Server Update data websocket event. @@ -444,29 +439,18 @@ func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { return } + // If currently connected to voice ws/udp, then disconnect. + // Has no effect if not connected. + voice.Close() + // Store values for later use voice.token = st.Token voice.endpoint = st.Endpoint voice.GuildID = st.GuildID - // If currently connected to voice ws/udp, then disconnect. - // Has no effect if not connected. - // voice.Close() - - // Wait for the sessionID from onVoiceStateUpdate - // voice.sessionID = <-voice.sessionRecv - // TODO review above - // wouldn't this cause a huge problem, if it's just a guild server - // update.. ? - // I could add a timeout loop of some sort and also check if the - // sessionID doesn't or does exist already... - // something.. a bit smarter. - - // We now have enough information to open a voice websocket conenction - // so, that's what the next call does. + // Open a conenction to the voice server err := voice.open() if err != nil { - log.Println("onVoiceServerUpdate Voice.Open error: ", err) - // TODO better logging + s.log(LogError, "onVoiceServerUpdate voice.open, ", err) } } From 609e7ad682b5aecc5d6434fbd62b7f3e43dbfc53 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 08:36:04 -0500 Subject: [PATCH 13/25] Logging code cleanup --- logging.go | 52 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/logging.go b/logging.go index f9d6e45..9c4fe00 100644 --- a/logging.go +++ b/logging.go @@ -20,30 +20,33 @@ import ( const ( - // Logs critical errors that can lead to data loss or panic - // also, only logs errors that would never be returned to - // a calling function. Such as errors within goroutines. + // Critical Errors that could lead to data loss or panic + // Only errors that would not be returned to a calling function LogError int = iota - // Logs very abnormal events even if they're also returend as - // an error to the calling code. + // Very abnormal events. + // Errors that are also returend to a calling function. LogWarning - // Logs normal non-error activity like connect/disconnects + // Normal non-error activity + // Generally, not overly spammy events LogInformational - // Logs detailed activity including all HTTP/Websocket packets. + // Detailed activity + // All HTTP/Websocket packets. + // Very spammy and will impact performance LogDebug ) -// logs messages to stderr -func msglog(cfgL, msgL int, format string, a ...interface{}) { +// msglog provides package wide logging consistancy for discordgo +// the format, a... portion this command follows that of fmt.Printf +// msgL : LogLevel of the message +// caller : 1 + the number of callers away from the message source +// format : Printf style message format +// a ... : comma seperated list of values to pass +func msglog(msgL, caller int, format string, a ...interface{}) { - if msgL > cfgL { - return - } - - pc, file, line, _ := runtime.Caller(1) + pc, file, line, _ := runtime.Caller(caller) files := strings.Split(file, "/") file = files[len(files)-1] @@ -54,26 +57,41 @@ func msglog(cfgL, msgL int, format string, a ...interface{}) { msg := fmt.Sprintf(format, a...) - log.Printf("%s:%d:%s %s\n", file, line, name, msg) + log.Printf("[DG%d] %s:%d %s %s\n", msgL, file, line, name, msg) } // helper function that wraps msglog for the Session struct +// This adds a check to insure the message is only logged +// if the session log level is equal or higher than the +// message log level func (s *Session) log(msgL int, format string, a ...interface{}) { if s.Debug { // Deprecated s.LogLevel = LogDebug } - msglog(s.LogLevel, msgL, format, a...) + + if msgL > s.LogLevel { + return + } + + msglog(msgL, 2, format, a...) } // helper function that wraps msglog for the VoiceConnection struct +// This adds a check to insure the message is only logged +// if the voice connection log level is equal or higher than the +// message log level func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { if v.Debug { // Deprecated v.LogLevel = LogDebug } - msglog(v.LogLevel, msgL, format, a...) + if msgL > v.LogLevel { + return + } + + msglog(msgL, 2, format, a...) } // printEvent prints out a WSAPI event. From 2b85edc4a1bc9e6be1122a68480d8fa70d6539b5 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 09:26:35 -0500 Subject: [PATCH 14/25] comments --- logging.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logging.go b/logging.go index 9c4fe00..a754dbd 100644 --- a/logging.go +++ b/logging.go @@ -32,7 +32,7 @@ const ( // Generally, not overly spammy events LogInformational - // Detailed activity + // Very detailed non-error activity // All HTTP/Websocket packets. // Very spammy and will impact performance LogDebug From dd69a7e27f1e97a21239dfbb65c5a39439561d1b Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 09:27:57 -0500 Subject: [PATCH 15/25] Added logging statements --- voice.go | 2 ++ wsapi.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/voice.go b/voice.go index b855aa5..44b3666 100644 --- a/voice.go +++ b/voice.go @@ -126,6 +126,7 @@ func (v *VoiceConnection) Disconnect() (err error) { // Close websocket and udp connections v.Close() + v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) delete(v.session.VoiceConnections, v.GuildID) return @@ -428,6 +429,7 @@ func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struc var err error ticker := time.NewTicker(i * time.Millisecond) for { + v.log(LogDebug, "Sending heartbeat packet") err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())}) if err != nil { log.Println("wsHeartbeat send error: ", err) diff --git a/wsapi.go b/wsapi.go index 1c6abba..996ec91 100644 --- a/wsapi.go +++ b/wsapi.go @@ -377,6 +377,7 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} err = s.wsConn.WriteJSON(data) if err != nil { + s.log(LogInformational, "Deleting VoiceConnection %s", gID) delete(s.VoiceConnections, gID) return } @@ -385,6 +386,7 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi err = voice.waitUntilConnected() if err != nil { voice.Close() + s.log(LogInformational, "Deleting VoiceConnection %s", gID) delete(s.VoiceConnections, gID) return } From a24f9e3d108e04b93d8a08b39d54f0ec5c5695b8 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 13:38:20 -0500 Subject: [PATCH 16/25] Slight better rate limit handling This improves greatly on the previous rate limit handling however still needs review and possible improvement. Please report bugs! --- restapi.go | 33 ++++++++++++++++++++++++++++++++- structs.go | 11 +++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index f740b42..99d3533 100644 --- a/restapi.go +++ b/restapi.go @@ -25,6 +25,8 @@ import ( "net/http" "net/url" "strconv" + "strings" + "sync" "time" ) @@ -49,6 +51,26 @@ func (s *Session) Request(method, urlStr string, data interface{}) (response []b // request makes a (GET/POST/...) Requests to Discord REST API. func (s *Session) request(method, urlStr, contentType string, b []byte) (response []byte, err error) { + // rate limit mutex for this url + // TODO: review for performance improvements + // ideally we just ignore endpoints that we've never + // received a 429 on. But this simple method works and + // is a lot less complex :) It also might even be more + // performat due to less checks and maps. + var mu *sync.Mutex + s.rateLimit.Lock() + if s.rateLimit.url == nil { + s.rateLimit.url = make(map[string]*sync.Mutex) + } + + bu := strings.Split(urlStr, "?") + mu, _ = s.rateLimit.url[bu[0]] + if mu == nil { + mu = new(sync.Mutex) + s.rateLimit.url[urlStr] = mu + } + s.rateLimit.Unlock() + if s.Debug { log.Printf("API REQUEST %8s :: %s\n", method, urlStr) log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) @@ -77,7 +99,9 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons client := &http.Client{Timeout: (20 * time.Second)} + mu.Lock() resp, err := client.Do(req) + mu.Unlock() if err != nil { return } @@ -111,13 +135,20 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons // TODO check for 401 response, invalidate token if we get one. case 429: // TOO MANY REQUESTS - Rate limiting + rl := RateLimit{} err = json.Unmarshal(response, &rl) if err != nil { - err = fmt.Errorf("Request unmarshal rate limit error : %+v", err) + s.log(LogError, "rate limit unmarshal error, %s", err) return } + s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) + mu.Lock() time.Sleep(rl.RetryAfter) + mu.Unlock() + // we can make the above smarter + // this method can cause longer delays then required + response, err = s.request(method, urlStr, contentType, b) default: // Error condition diff --git a/structs.go b/structs.go index 18e24f5..94995bb 100644 --- a/structs.go +++ b/structs.go @@ -75,6 +75,17 @@ type Session struct { // When nil, the session is not listening. listening chan interface{} + + // used to deal with rate limits + // may switch to slices later + // TODO: performance test map vs slices + rateLimit rateLimitMutex +} + +type rateLimitMutex struct { + sync.Mutex + url map[string]*sync.Mutex + bucket map[string]*sync.Mutex // TODO :) } // A VoiceRegion stores data for a specific voice region server. From 2ac4665a4e072437d3d2557074d8f5bd0cf6bd2a Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 14:22:43 -0500 Subject: [PATCH 17/25] Clean up Gateway API Event handling. --- wsapi.go | 71 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/wsapi.go b/wsapi.go index 996ec91..b7da19c 100644 --- a/wsapi.go +++ b/wsapi.go @@ -167,7 +167,10 @@ func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { case <-listening: return default: - go s.event(messageType, message) + // TODO make s.event a variable that points to a function + // this way it will be possible for an end-user to write + // a completely custom event handler if needed. + go s.onEvent(messageType, message) } } } @@ -245,73 +248,83 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) { return } -// Front line handler for all Websocket Events. Determines the -// event type and passes the message along to the next handler. +// onEvent is the "event handler" for all messages received on the +// Discord Gateway API websocket connection. +// +// If you use the AddHandler() function to register a handler for a +// specific event this function will pass the event along to that handler. +// +// If you use the AddHandler() function to register a handler for the +// "OnEvent" event then all events will be passed to that handler. +// +// TODO: You may also register a custom event handler entirely using... +func (s *Session) onEvent(messageType int, message []byte) { -// event is the front line handler for all events. This needs to be -// broken up into smaller functions to be more idiomatic Go. -// Events will be handled by any implemented handler in Session. -// All unhandled events will then be handled by OnEvent. -func (s *Session) event(messageType int, message []byte) { var err error var reader io.Reader - reader = bytes.NewBuffer(message) + // If this is a compressed message, uncompress it. if messageType == 2 { - z, err1 := zlib.NewReader(reader) - if err1 != nil { - log.Println(fmt.Sprintf("Error uncompressing message type %d: %s", messageType, err1)) + + z, err := zlib.NewReader(reader) + if err != nil { + s.log(LogError, "error uncompressing websocket message, %s", err) return } + defer func() { err := z.Close() if err != nil { - log.Println("error closing zlib:", err) + s.log(LogWarning, "error closing zlib, %s", err) } }() + reader = z } + // Decode the event into an Event struct. var e *Event decoder := json.NewDecoder(reader) if err = decoder.Decode(&e); err != nil { - log.Println(fmt.Sprintf("Error decoding message type %d: %s", messageType, err)) + s.log(LogError, "error decoding websocket message, %s", err) return } - if s.Debug { + if s.Debug { // TODO: refactor using s.log() printEvent(e) } + // Map event to registered event handlers and pass it along + // to any registered functions i := eventToInterface[e.Type] if i != nil { + // Create a new instance of the event type. i = reflect.New(reflect.TypeOf(i)).Interface() // Attempt to unmarshal our event. - // If there is an error we should handle the event itself. if err = json.Unmarshal(e.RawData, i); err != nil { - log.Printf("error unmarshalling %s event, %s\n", e.Type, err) - // Ready events must fire, even if they are empty. - if e.Type != "READY" { - i = nil - } - + s.log(LogError, "error unmarshalling %s event, %s", e.Type, err) } - } else { - log.Println("Unknown event.") - i = nil - } - if i != nil { + // Send event to any registered event handlers for it's type. + // Because the above doesn't cancel this, in case of an error + // the struct could be partially populated or at default values. + // However, most errors are due to a single field and I feel + // it's better to pass along what we received than nothing at all. + // TODO: Think about that decision :) + // Either way, READY events must fire, even with errors. s.handle(i) + + } else { + s.log(LogWarning, "unknown event type %s", e.Type) + printEvent(e) } + // Emit event to the OnEvent handler e.Struct = i s.handle(e) - - return } // ------------------------------------------------------------------------------------------------ From 94770635a988ae75b95a21bee212f917896796c5 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 16:59:45 -0500 Subject: [PATCH 18/25] Event handing improvements. Corrected the Event struct to match it's new state based on Discord docs and started tracking sequence number and sending it with heatbeats. Also, move cleanup and comment improvements. --- logging.go | 6 ------ structs.go | 8 +++++--- wsapi.go | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/logging.go b/logging.go index a754dbd..6c59760 100644 --- a/logging.go +++ b/logging.go @@ -94,12 +94,6 @@ func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { msglog(msgL, 2, format, a...) } -// printEvent prints out a WSAPI event. -func printEvent(e *Event) { - log.Println(fmt.Sprintf("Event. Type: %s, State: %d Operation: %d Direction: %d", e.Type, e.State, e.Operation, e.Direction)) - printJSON(e.RawData) -} - // printJSON is a helper function to display JSON data in a easy to read format. func printJSON(body []byte) { var prettyJSON bytes.Buffer diff --git a/structs.go b/structs.go index 94995bb..d9254bb 100644 --- a/structs.go +++ b/structs.go @@ -80,6 +80,9 @@ type Session struct { // may switch to slices later // TODO: performance test map vs slices rateLimit rateLimitMutex + + // sequence tracks the current gateway api websocket sequence number + sequence int } type rateLimitMutex struct { @@ -284,10 +287,9 @@ type FriendSourceFlags struct { // An Event provides a basic initial struct for all websocket event. type Event struct { - Type string `json:"t"` - State int `json:"s"` Operation int `json:"op"` - Direction int `json:"dir"` + Sequence int `json:"s"` + Type string `json:"t"` RawData json.RawMessage `json:"d"` Struct interface{} `json:"-"` } diff --git a/wsapi.go b/wsapi.go index b7da19c..32c0360 100644 --- a/wsapi.go +++ b/wsapi.go @@ -196,7 +196,7 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} var err error ticker := time.NewTicker(i * time.Millisecond) for { - err = wsConn.WriteJSON(heartbeatOp{1, int(time.Now().Unix())}) + err = wsConn.WriteJSON(heartbeatOp{1, s.sequence}) if err != nil { log.Println("Error sending heartbeat:", err) return @@ -291,10 +291,19 @@ func (s *Session) onEvent(messageType int, message []byte) { return } - if s.Debug { // TODO: refactor using s.log() - printEvent(e) + if s.Debug { + s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) } + // Do not try to Dispatch a non-Dispatch Message + if e.Operation != 0 { + // But we probably should be doing something with them. + return + } + + // Store the message sequence + s.sequence = e.Sequence + // Map event to registered event handlers and pass it along // to any registered functions i := eventToInterface[e.Type] @@ -318,8 +327,7 @@ func (s *Session) onEvent(messageType int, message []byte) { s.handle(i) } else { - s.log(LogWarning, "unknown event type %s", e.Type) - printEvent(e) + s.log(LogWarning, "unknown event, %#v", e) } // Emit event to the OnEvent handler From beec086d698eb255f49197e2713c2cfb6177aae3 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Fri, 29 Apr 2016 00:40:42 +0200 Subject: [PATCH 19/25] Fixed ChannelMessageAck --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 99d3533..39ba567 100644 --- a/restapi.go +++ b/restapi.go @@ -1021,7 +1021,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID // messageID : the ID of a Message func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) { - _, err = s.Request("POST", CHANNEL_MESSAGE_ACK(channelID, messageID), nil) + _, err = s.request("POST", CHANNEL_MESSAGE_ACK(channelID, messageID), "", nil) return } From 098d7861a417db4f467241d128be4c0d4863e672 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 17:41:05 -0500 Subject: [PATCH 20/25] BREAKING - Added RateLimited event Renamed RateLimit struct to TooManyRequests{} and added new event struct RateLimited{} which can be registerd to with AddHandler() and will be emitted anytime a HTTP 429 is received on the HTTP API. --- events.go | 6 ++++++ restapi.go | 4 +++- structs.go | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/events.go b/events.go index ce1f815..23aa6b9 100644 --- a/events.go +++ b/events.go @@ -50,6 +50,12 @@ type Connect struct{} // Disconnect is an empty struct for an event. type Disconnect struct{} +// RateLimited is a struct for the RateLimited event +type RateLimited struct { + *TooManyRequests + URL string +} + // MessageCreate is a wrapper struct for an event. type MessageCreate struct { *Message diff --git a/restapi.go b/restapi.go index 99d3533..10f721f 100644 --- a/restapi.go +++ b/restapi.go @@ -136,13 +136,15 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons case 429: // TOO MANY REQUESTS - Rate limiting - rl := RateLimit{} + rl := TooManyRequests{} err = json.Unmarshal(response, &rl) if err != nil { s.log(LogError, "rate limit unmarshal error, %s", err) return } s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) + s.handle(RateLimited{TooManyRequests: &rl, URL: urlStr}) + mu.Lock() time.Sleep(rl.RetryAfter) mu.Unlock() diff --git a/structs.go b/structs.go index d9254bb..2721fb3 100644 --- a/structs.go +++ b/structs.go @@ -318,8 +318,9 @@ type Relationship struct { ID string `json:"id"` } -// A RateLimit struct holds information related to a specific rate limit. -type RateLimit struct { +// A TooManyRequests struct holds information received from Discord +// when receiving a HTTP 429 response. +type TooManyRequests struct { Bucket string `json:"bucket"` Message string `json:"message"` RetryAfter time.Duration `json:"retry_after"` From 38c51ce7883951848bab4e8464d5c13bf4a9aafa Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 17:43:41 -0500 Subject: [PATCH 21/25] Renamed RateLimited to RateLimit This is more consistant with other event names. --- events.go | 4 ++-- restapi.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/events.go b/events.go index 23aa6b9..23fa9be 100644 --- a/events.go +++ b/events.go @@ -50,8 +50,8 @@ type Connect struct{} // Disconnect is an empty struct for an event. type Disconnect struct{} -// RateLimited is a struct for the RateLimited event -type RateLimited struct { +// RateLimit is a struct for the RateLimited event +type RateLimit struct { *TooManyRequests URL string } diff --git a/restapi.go b/restapi.go index 10f721f..302866e 100644 --- a/restapi.go +++ b/restapi.go @@ -143,7 +143,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons return } s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) - s.handle(RateLimited{TooManyRequests: &rl, URL: urlStr}) + s.handle(RateLimit{TooManyRequests: &rl, URL: urlStr}) mu.Lock() time.Sleep(rl.RetryAfter) From f6de2b2c98b7751131b9e44c080d6691ebecdc0e Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 21:15:32 -0500 Subject: [PATCH 22/25] gateway, voice, & logging improvements We now store teh sessionID of the gateway connection for later use. We are now caching the gateway url and will use it until unable to connect to that gateway. We're not longer smashing the VoiceConnections map during reconnects which was causing Voice problems. Slight change to log output format. --- discord.go | 4 ++++ logging.go | 2 +- structs.go | 6 ++++++ wsapi.go | 50 ++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/discord.go b/discord.go index 99a343f..72c848f 100644 --- a/discord.go +++ b/discord.go @@ -237,5 +237,9 @@ func (s *Session) initialize() { // onReady handles the ready event. func (s *Session) onReady(se *Session, r *Ready) { + // Store the SessionID within the Session struct. + s.sessionID = r.SessionID + + // Start the heartbeat to keep the connection alive. go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) } diff --git a/logging.go b/logging.go index 6c59760..7c487ed 100644 --- a/logging.go +++ b/logging.go @@ -57,7 +57,7 @@ func msglog(msgL, caller int, format string, a ...interface{}) { msg := fmt.Sprintf(format, a...) - log.Printf("[DG%d] %s:%d %s %s\n", msgL, file, line, name, msg) + log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg) } // helper function that wraps msglog for the Session struct diff --git a/structs.go b/structs.go index 2721fb3..dd7a1fc 100644 --- a/structs.go +++ b/structs.go @@ -83,6 +83,12 @@ type Session struct { // sequence tracks the current gateway api websocket sequence number sequence int + + // stores sessions current Discord Gateway + gateway string + + // stores session ID of current Gateway connection + sessionID string } type rateLimitMutex struct { diff --git a/wsapi.go b/wsapi.go index 32c0360..077d092 100644 --- a/wsapi.go +++ b/wsapi.go @@ -50,6 +50,9 @@ type handshakeOp struct { // Open opens a websocket connection to Discord. func (s *Session) Open() (err error) { + + s.log(LogInformational, "called") + s.Lock() defer func() { if err != nil { @@ -57,7 +60,10 @@ func (s *Session) Open() (err error) { } }() - s.VoiceConnections = make(map[string]*VoiceConnection) + if s.VoiceConnections == nil { + s.log(LogInformational, "creating new VoiceConnections map") + s.VoiceConnections = make(map[string]*VoiceConnection) + } if s.wsConn != nil { err = errors.New("Web socket already opened.") @@ -65,27 +71,41 @@ func (s *Session) Open() (err error) { } // Get the gateway to use for the Websocket connection - g, err := s.Gateway() - if err != nil { - return - } + if s.gateway == "" { + s.gateway, err = s.Gateway() + if err != nil { + return + } - // Add the version and encoding to the URL - g = g + fmt.Sprintf("?v=%v&encoding=json", GATEWAY_VERSION) + // Add the version and encoding to the URL + s.gateway = fmt.Sprintf("%s?v=%v&encoding=json", s.gateway, GATEWAY_VERSION) + } header := http.Header{} header.Add("accept-encoding", "zlib") - // TODO: See if there's a use for the http response. - // conn, response, err := websocket.DefaultDialer.Dial(session.Gateway, nil) - s.wsConn, _, err = websocket.DefaultDialer.Dial(g, header) + s.log(LogInformational, "connecting to gateway %s", s.gateway) + s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header) if err != nil { + s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err) + s.gateway = "" // clear cached gateway + // TODO: should we add a retry block here? return } - err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) - if err != nil { - return + if s.sessionID != "" && s.sequence > 0 { + + s.log(LogInformational, "sending resume packet to gateway") + // TODO: RESUME + + } else { + + s.log(LogInformational, "sending identify packet to gateway") + err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) + if err != nil { + s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) + return + } } // Create listening outside of listen, as it needs to happen inside the mutex @@ -292,12 +312,14 @@ func (s *Session) onEvent(messageType int, message []byte) { } if s.Debug { - s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) + s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) } // Do not try to Dispatch a non-Dispatch Message if e.Operation != 0 { // But we probably should be doing something with them. + // TEMP + s.log(LogWarning, "Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) return } From d557bb24a54bccf32a869acc94f2a3ed3dbd5735 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 21:50:57 -0500 Subject: [PATCH 23/25] Extra logging for non OP0 events, for now. --- wsapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsapi.go b/wsapi.go index 077d092..acfc3eb 100644 --- a/wsapi.go +++ b/wsapi.go @@ -319,7 +319,7 @@ func (s *Session) onEvent(messageType int, message []byte) { if e.Operation != 0 { // But we probably should be doing something with them. // TEMP - s.log(LogWarning, "Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) + s.log(LogWarning, "Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) return } From 53a826dd0d5c0a753d41c8dbd44516d42f6b158e Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 28 Apr 2016 22:30:42 -0500 Subject: [PATCH 24/25] Send heartbeat in response to gateway Op 1 message --- wsapi.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/wsapi.go b/wsapi.go index acfc3eb..256b655 100644 --- a/wsapi.go +++ b/wsapi.go @@ -315,11 +315,22 @@ func (s *Session) onEvent(messageType int, message []byte) { s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) } + // Ping request. + // Must respond with a heartbeat packet within 5 seconds + if e.Operation == 1 { + s.log(LogInformational, "sending heartbeat in response to Op1") + err = s.wsConn.WriteJSON(heartbeatOp{1, s.sequence}) + if err != nil { + s.log(LogError, "error sending heartbeat in response to Op1") + return + } + } + // Do not try to Dispatch a non-Dispatch Message if e.Operation != 0 { // But we probably should be doing something with them. // TEMP - s.log(LogWarning, "Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) + s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) return } From b09ed3729416289feed7069ba5a051b32e0571be Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Fri, 29 Apr 2016 14:57:41 -0500 Subject: [PATCH 25/25] reconenct with identify packet until resume is fixed. --- wsapi.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/wsapi.go b/wsapi.go index 256b655..d09f07c 100644 --- a/wsapi.go +++ b/wsapi.go @@ -97,16 +97,16 @@ func (s *Session) Open() (err error) { s.log(LogInformational, "sending resume packet to gateway") // TODO: RESUME - - } else { - - s.log(LogInformational, "sending identify packet to gateway") - err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) - if err != nil { - s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) - return - } } + //else { + + s.log(LogInformational, "sending identify packet to gateway") + err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) + if err != nil { + s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) + return + } + //} // Create listening outside of listen, as it needs to happen inside the mutex // lock.