From d29cd0438757f693c6f4f4ec30f56d5c7cbfb580 Mon Sep 17 00:00:00 2001 From: David Rosa Date: Tue, 19 Dec 2023 15:43:09 -0800 Subject: [PATCH] integrations: Create incoming webhook for GitHub Sponsors. Creates an incoming webhook integration for Github Sponsors. The main use case is getting notifications when new sponsors sign up. Fixes #18320. --- .prettierignore | 1 + .../integrations/githubsponsors/001.png | Bin 0 -> 30226 bytes zerver/lib/integrations.py | 28 ++++-- .../webhooks/github/fixtures/cancelled.json | 78 +++++++++++++++ zerver/webhooks/github/fixtures/created.json | 78 +++++++++++++++ zerver/webhooks/github/fixtures/edited.json | 83 ++++++++++++++++ .../github/fixtures/pending_cancellation.json | 91 +++++++++++++++++ .../github/fixtures/pending_tier_change.json | 91 +++++++++++++++++ .../github/fixtures/tier_changed.json | 90 +++++++++++++++++ zerver/webhooks/github/githubsponsors.md | 22 ++++ zerver/webhooks/github/tests.py | 57 +++++++++++ zerver/webhooks/github/view.py | 94 ++++++++++++++++++ 12 files changed, 707 insertions(+), 6 deletions(-) create mode 100644 static/images/integrations/githubsponsors/001.png create mode 100644 zerver/webhooks/github/fixtures/cancelled.json create mode 100644 zerver/webhooks/github/fixtures/created.json create mode 100644 zerver/webhooks/github/fixtures/edited.json create mode 100644 zerver/webhooks/github/fixtures/pending_cancellation.json create mode 100644 zerver/webhooks/github/fixtures/pending_tier_change.json create mode 100644 zerver/webhooks/github/fixtures/tier_changed.json create mode 100644 zerver/webhooks/github/githubsponsors.md diff --git a/.prettierignore b/.prettierignore index 81de172073..a9fdd4d0c4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,4 +8,5 @@ pnpm-lock.yaml /web/third /zerver/tests/fixtures /zerver/webhooks/*/doc.md +/zerver/webhooks/github/githubsponsors.md /zerver/webhooks/*/fixtures diff --git a/static/images/integrations/githubsponsors/001.png b/static/images/integrations/githubsponsors/001.png new file mode 100644 index 0000000000000000000000000000000000000000..84412ebeda2c7c66401171fa327c3e0bec8e186e GIT binary patch literal 30226 zcmd42by$^M*FCxsrMo+o4w3E#1wlZ%L+S1g2|-Fkq#FbzrMp8Kq-&GXvFV22;(L9* z=RJS_u5+%lFMIEOd#|At8WY^O#$) zz;AF)DzXxgk|DA!2!smqN=jVSJ!N;n%^h#z4guy?(J*tOj~sy2N`>`FMIx4lD$5^F zTpC-t=^Le|G8{gB=?CLB$5J_EC6<@CF-#~(FL69^kf^D}JWG?SldZ3Bv0bMJZNDsh zE)O_A77}@5x;`+F_zu?_>a^}9m7AL@fQ9|PuNZ}&Ry!E~{SS-?7gGPectYVs{`bS! zG3dtrZ~lGrT_DjC?!WKOQ2k$g^_7!;QGFI!-;rZJp4v$M_{a~nkuB#z4^*0eU&&NB zVTSZE5-f3I+nbLOHF$sN>LwjrfhF;NE!%MxP{k+AZluE0uBxxQlK%HL$M>gSJ+(hQ z{urA_C`B`hu2Ll)lcjkX1Lwb%MU$$C z7%2l^B@?*4ZOKqrm^m>ivv2I>ReOop+BVqK%kL2j?=5Y?C|CXhl8RyclRPCgWodV9 zJV)`r_H_Y`sqlxAmfDj@8Pu4UkE53q?VX+Mepe;oEitd7HFR(8?eU}A=x|GQqPsZc2z zcTe}$t{Gy8(wDdZlHm639Zmctuti_reTNqyPaz{;+Fr3fMGp)8-`|;3FTkd^ZTku9^Nr{Y_!%wvhZL%dR zZj!GX8;>9PnKxt_>}+CFcIM5lb$t@VzCfSgl3L|5~oj=fKwp zKc_LYBN6&u)cwzgH8j*8LEfPq{MW4(WerbS{rJHm(z60Fc>5L}V*l>{5tjHq?@3); zs=78bDV7Q?;k^q~E#KGt_nq6EO_jSuYTF$0e?z>OVq?MyE7~Ra_kKr<71E2XCYZv9 zV>IVg@+#^j2mkyoS^P{){)ArC;3QF)P_fxpric0X*#2$)A#->&o<+~ zS5dxo)-LX{VNDBIR7Z(rqdA39=NHB?^#2lDvC~nQeQLWHQa*r|A19Y{;{%b7$C=B> zlad-w+X8W{u0~y&0U@55`Soi^MqeBbgieN4hq(3MovL@y5`k}tWI#cQsAfhvQkYdG zAU-JU8~%81BdlF%ASE^Us&FE!zMkal1#zbG5wKe@=uK+{@TTGkn!Uzxe=5|SReY5HA> z$$|eF{*eKnheJ`573v*EdMg6x?no7prGW zvB`qft1@wal9$gM+rs+ISj*a=_&3sPkPt4483cJT?R+mdN^)|Ill(Kp@Tf`qQ_o=Yun9;a@dW4R?!VgZ8F2TKo~|=Et^gsVqw^JFfAJzSX43 z6gCw|c!NZbdHK$%ViGkxJQxNADWoUx1huUxO^POvk_moUq4w0&WBddQQhjnN`MJ?th~q>-!Sgjk>B^byMBUc~`DutdEqllQKhG zeo@hf=o-RG0rD3WP6O3r=2f*{l0wRC^_Hn(s~p+wbbZCC%N?EI#=m9NrF{8ht0%Fv zwmID`4ByAZ6x0?g=I0mMwIVYd6WWCYJD1d!p;NWw?(6GcUfzfP)_eA!|J;(`z~}5q z`SB5p?ZM7oq#I$gak)X>7&V;;MbB_I6)Ptyeq3BxO^vL7Df`vgb{KXb0*|Kh(lx7& z!-{ppsE8~gVr@emb?X!%{Pn50do;yoxm`JwM|~UjeG~M!mmcYSktt!%hiYe`0%JAeE6Gg zlW}EnL5a-OPg=Tb5&_vakFis3ZkBv>qDglh#s)m>1;7?OL}=jkdB;M>$HYp;IL=nP zfw*ziO=kh;o!_~|4HECTJIeXBQwe-UL?B9bp`{a+%Bs2_GFts7ch}K5(GE_zNYL5x za7E8t+{?jVVwN%p7_u_U2GPQw*wl+C^OTq}m6&3Z)1#+R36~Cb!s#iK!W6~D zBhyH|sN38H?>@Zpw=*;Q=-?LF+P&xsfndl|@{%r53Oh(#T%{?~vhMrITOLF1A3)6W z;&a!Ynp)h;8ylTKo1+!u&xC|;4-VO~d$O)?-?B8DIUOFsT6^qREy@&nt2_tZr8O3z z^=iC@K-5s4T%GLBbPLmz>ndSK4Bw%;f{`|SH-s9DT4U9#GTm)X7O4=(SKES0Q zJ0|k0UL~vXU(5`x^o5g#%g4hswUQq_hlkl&aIVHUo?|8Q^Yf)AAXRDGs4(%aMBPn0 zEm48%y{vUcz6_Rz9Ul@znXRua$yt&>sGjl>8R<3fOrKBwUWfBFbyh80i&fd9|Ha|6R$cDTfju~V4&p3_RZ7bc?40_0I?d;!Gg3V2bJQJ)B!tK(+Q{m~y^M2Uzn4EI@2cnMryKw;b% zveG8!0fDs%DG@6w9!=rI0)qU@Ba;dcf7;QWpXM{y?LTc6iwI;s@23hQcID>EY)}aM zJ_6{+Vts$T<>7t-Hx_`{V`Zf2YD?ieU5~D8Hk4JH^EhpPpGd?V>+W=u*0f(JB1r@n zB^L(*E0r}{?fq!RT@aZz0Ht{5$j@WD7$6y={EM9>s3xAi-Rq<^g?MaK_@rANIz2OE zWZ2Tohfg}iM12=l!7*KJ#?JOs??YoFnPOBF(%v|7c8HAO(S?J|X(O6nb8Js^$CvnZ z+GUj&Ja^UbW<#m6@73@h-FS0YFF^*8N$C_pAVAUd9Ke-}3jR5+6} z?)+*M-o}I9b)1ezM;Q(3+(pe;i!;60dXf@U)y5(2PQNXy)CUzomf)r3ilkBO!_J!y zTiST#3JY$NZ3d6G!n#~#rl|fCo%J(nQ{zi7PaKTAh9b7;X2NUo=NKx<^OP9k zK<*Y#{HH4Qh5X?gG;p`c);Cj{LsHw{)##Sd@c;RVgv?O+HB z3Yu4Gcj~`R&?`FWgokW!m~Esp9@@E{-A7^Bzjg5_65;vvI z_i&=t+-y^->iSUtt+Bk8ANfvSF^hQapa-2l*L%C{Xvrj^Uxm=JKZBaOe}yOrilarM z*0kRR!(GF!=q3SH&CZFx3u*ieWSdRkjO;avcfMvw?R zzQ(vkhwwS=H>$yjKx+B;mrL3LgMuyIbay8<8N4bWDr~?x4#T5u;+qC>5&_ew$VB5Ul!_^l;w}m{1@nir2jyYA5~YLi-tqpB`O6{; z|NMC0-B9I0ufzt#ea~Z28Jj3J9hCR4ewHnqrg@E8xc4`%!)ADQf?)bA1SUtdKWmGv zG<3l>w%lLFKgac(*Q3SS+(1k$y3>$CizMeD!&g%a6>>p2DtCnVw?t#^@6O4Zzcm=l z?`JG)YRUtq^B2oOo$s}p!&E#>O%(P7&V+u zS5BrqceYE2?A;4ckTT0^iIS|I@qUt%E8A>3|7mfmK?l!#uX9h%b9dHI#!3*54Q}4U zqpl3L4(>^);lkzSqJ@2FNALdiod#5o?r|4Q%rA2>2*mZ~Cne{8viXUt)Z6sibX@ww z*=y#Av&-u%iNev0jM}sFilRoc(&ien^p*xZY`1x3yO!7z-L4;$zAsP?Qpw1%JohS& zK`7$4dx6!23SM<|45Dk*dGNU%(tw&S;AZU|6Z_57!oAdt2;hcC)r=@X;tYTLk6plw zYX3t?x6u#hbld^s%2c27t1T%NK^6V4!w@2;DT2bbzIs-@UfJ#Jy>N8zY7g?sA&(ja5iQcu2sgz7Qs?C}93k#&qu1%~?BHO?1U*pfd`^%7h4!EI zRA(h$f^Qvob#4Ofz-;SSKxc|W5w*U?xAL1s%5XjGQxm-jjfQ$>YwbUadHFoUz#t_m z(&`NBeo3?^sIW7@) zpXb8rmDO!adRzMnrunjA&3*^ai{bY8<%3oDV)gLFyXojR>cs)-h0{j$6PWpXflIU>AmO4H7GWBQJ;8FV97_jxsLo$t;@u+y`8;ae7i=r z8}Vp<%IGQ43oKBf0g?FTd&5}m(|}OoLLfOhce!Bz@fWE%Z>?~mkgWMUl&``-MG&s6 zsT;u;IMF5Qd5n%m$|p8G#Lm**X3{h8E!k25V2daCPe69ev1|_Y)looqCPcz7v09pf z&mkZOSiiD6+S`|1dEfF2kb}R&%gXMQivLF+j;DRq>t}}J(k7UlGBnck@Bnv|I+5rf z-*f7?gfvaIO{Es?eCWxwzH#CB^k?v-UsO-iAJrRqoJy0>v=jwA`J;S-Nn`uZ8JQlC5 zuSG(A5kVm!W$wf++|+e_c(VKPu&-jcwPT^U+CYVIUHAqo1f%w?xzo*nmXW%z6VNVneut?W5b_aEw+yiWzQF`GuBlww@=HxEGDh~MFe=#KJSG&iInp@w)pzdzV%P1btPhNrKiRmIWDGI0>9o#Fa~cYC8-xi?=G*KR%57Y1YT|b z!N))KHB6TG$+pY>h>qV}Qc}>CTd0wI+Pp4tlhdzZyP4F~QSx!vkAnXbDDIh1!wQjR zD3tCRaEIrehGMIL@hn>ftAuTGng#E9$wu=CN-o&DSVQ=nS<0azCnyc*z9QYXfZ=Wa zE{HADQ`fh3Ze$$x-PHUFb2(?z8!3(7(!mP6I_G4va$;Kf8U;0po(sc@QA>_REbhuHKhU1#pDgv08H@BtN z*OLFq0%uM~*LsS44K+p*a|@wM6{Z+2i=&ifVSq)kpCAr|I6*ioS4aGwHH+$khn2u@ z>8m2Sbn)`(Y9oIXT&f1V31;SHki?B2B(keeJ)x$)AuYhT zx$*Id(NKb9xMmt!@e@USokVX22*!Uobq#pW9WSnDa4*}QP3v^x!oc)1CRPdZ|ljy&xKtV<5gTN%jYo@BemGH0Cmv( zXHWNK&?v@pnyV{$(gXbV^UxXUzaqVN;Q0FN`+dQWh(PvKb*tYp8fM=ce*E~oTlKZ8 zi{Z6gXI*-Wor;0*`~t`7-F493ebEh>ADPGpW($jHFBlf|dKYzhI^vo>siBj#z_x5c z6K2Mms`$|f8Tr>>DSG9?!hBFn^>@e5y|6#m2g@-9ZhRdHQzu(P zDzgEY(ZyBZ`v)t>$iTsZk;i%_q~MK6AHMLB?^Y3+B8Z;ca+bL_k@zPIQ1<4vE(An43fkl-9SBP`QZ`MMs5?mx z!(L4t_!Kuc=>y6rufbB-jEzfaaKPb1Y@gxUBuF1vE?k{6XtuRbFa1$DsPyl;(kYegZqa!3Lfns&;5W6A2{UYZ`fzrP+5aC^nm zpNtTNf#JSY;NgChsWlC%;q+mTZ;sh}c*yS;6|j|LyHxnLEv*pQqTd~( zG5b@)xqV?iGJNznZS+W)K!uhADX_d}?1L?tUYdQ+8xbLXqKmG?(4D&a@sJCRvF-FQ z14vr#?44ZdGP}&k$;p=Xw^2alL~evFZ%N%HpT7%9xPnj{Sg=}TJI(X1{(eH9;34S!q2ZtzY~ zKIA+^S5No3*R1M;miNM>Xb1KX5YS?A5@80eH&MU~Hk>#eB8{NBy1qgYMHHp91MQRL z4PTDS7v0f~{cAT_UvSn)zNT+sEiHcSDG$vZrr6w|e|&-58^6<}|Dnw+vrYu@*7H=V zMPE2ZLo*cLz&A4gZ5|w?;ZDFm&s21N<&QI3hgaeNg=N+E(OH=P9$+<9)V^lA3i1_v zoi%@8&2pJypV~eQzyE_DYGa58#R>7tdixvL8C>J0vOE7sr65(NE@9e(_JiyD#}Mlq z8$>cPvXPpNJ&EKGSu{nLhi0k<%r*;*`S9&tL5WTJJ}93)m7V29wrAC~kqcy)xvk2P zavTx{m2L>}!7nr<3>^W@-?UDbxVYAg#50 zYRV|kLy@z(GcCjU<57N#cQ7>W;&j#zNKwtrFyw}XwS-)ei$x6v7Pf%&8s16o%B-0V z-VRt^eol^7sa||xc*S!`K5p8vwTqG#45H%a3wV9omb;B-;ph-(1oub*!;4J4PUzRACw__I+{jV%(k zUP~DJQBtz#RAQM>q&%za9qP3?gorZJBU)A=V7wr%cR>yS?%JQP;|PQkRi`lh;fb!U z6|BW?9bOqct)jL$bu1jR2+oZm`25rWAL=a}&)|j0Us?*dvjwqO{<3KTk?{9q z9lt``l+-m$`b!or);A1_>Y>0?T-w@?S`I{zU?IrP$VcjLxu84>{}jaY_8`^1;}&2m zpWmTzc*wb^-90+}Q1?=9-bQAjcqDtI4DI6yvAqxFr&@Pi@$L>#+;-#2uz2>>Q%n&& z)9LUoxjpvP zES<}irFpLLN!*m&8jIE;hlqz2GSF=(8D|sUYees)%n#PLKOtw!au@gksi3cUMDSg) z7BTjK*sfCZU4dNq==-=&%XNpPyJE*`0h z^k4ZcZE?bg^anL5BK`w(za~Swj2lnJV=r7+>2zQID1IK37#lc{LYD0cyh?Z1p==}+ zH1m@runS12y(CBHr(K*ClpF;@4%8g45|$T_tM|()I9occ0I=8cO)2UR^$yG0io-qf z_F?r@)G@(@^Y!}Owuo8cZ|(WdEWno53ObHSE1Xz~+1SVi4Gw2y4A3c;@izW4I^g8QU43G_vU=G0L1Sre z3pq?PqEUd^c{q_{fq@FqKU8qPRq`#J$=HD)9E9G)gya3b(BmhMGb)P)%!T1VQ>5|s zs3mzd=81xjE2bOogUzH_x$6U|LJbxZ8`Ks#O zXo;?Vf5CY*4~5sKpkQEP?dna?%+$MwHjQ#TFMD-vj#8zKo?A|097Y6;UsK7OGXm%i zH{MFkM;Ha@%IuPm+5gl2?B@UV&fRFAth3ud>}00mW&ePL^!sPKxhjt@E;9{E(}+0S zKTp`@_wVk3LKq?@DhfxH@%M90VK0Z=3*q*KjQL#U(w3MxHg6ZDS z*;t{$iYwQ;E-%i1--~6cX31qr0gO0Czn_l2_ido)H!z5xc7qxno+1L@0c70DbT4SN z#A!zc$yb#Ir+kXEnuPeZ=f_M}dtm~z-k?g05-JGp^|tF?($uwDn)C2M@c6DFSARrzWQX=vM^+xVD<^FOmsUm@FQgWThpuNB;#?Jr#Qf& zrR+%cY2Rs5Y|(E5Ugd`wm6x3R>7iy6KApQB>_c9UUL*pkwEHk2I_B_^`V3}Y>_=U-EX0d67aIdrln^9rp;on`9RSHMNU@SfuoO^DM*|_ zGWCPN(n4@4*CmBQOl*8~N~8D1^;L$X7z9itjJ7Qr5S^HpE;pss0aX)pJ~?8}RaQ}f zoAxSiOuTSRTz5G1?<|V4*V)8%f02mnQ=Cjc_X9dsB~*x$nMi0IC#Ci8x;H{xxZr1n z)-32r7)^XSPacn+?1U`J^(9$SKi|h%if+#`29m^dPMrzQ{d{crr&Wh%p~U*1Qr7n9 zx$4!@Y}uI9WJE77FJ~l}-IQUyz_R^oN071|5?gyoj>#%)%w-av9#Df8R z<8XNmz_&L;DHiID*VkU5V{8MA61u-2iy-E~YuTHN28d7FQie68sJIC%Sh@&`FaVHX z!o_M|wq@Cj(rpUqWN!vn5codX^omr1^n|r%bUpc**af zvwPl9ZS|@B>hE7(SqmB>4}5O9iS@e0js~>Zd7aZqL0cY7wLC*JtUUjpD)|I77Cr-^ z1!^@&odwlpA3#0V3N?U?j0RvPhPdP?hhk}ISu6I>fJhaxvV}!~bpOTso^*J~u$Pi4AiC2i&MA< zgqG<6?ml2WhkM-yWgD|-rjY6Kbt~un>7O&#lGT4e+afav4YGT&uk-k-YX1PWs(G!D zvE6uNeV%3cwpa{w@Cq%!JeFX?Yyk4@971NQAoq0={a=OVyfYe~v_H}iExl*cOy`f3 zRWCTeZF!MmdNfu#2fA!<5Kj69Ge=#fxGYJ3(eG1?POJOdvoyEVbEdX%Z|kd`H`*D@ z1Jx7^+8wD_P%kgf=Z*A#dwx7d$IqO}c8V}|cTEJcXF#)}Z)JxQma1(oZ3G-z*y&~G zQ?M_|m80fdps7?-Mh>M#quAeP{Y_bJBakZ1UZW&Ph%=8p>*+hG5|#s<80qD4*`LM+ z1&V>W6e8uH9F98Aha~@ZDK?$!@ZRZVbEB6fqrU{O(Pb$OP7(wl^<0G8nFj3X_wUXJr~sB{$F! zpc`4NL=gUWUI&|-g`S-fGLq>=89wXeBK@Dk`JZ1&JOAm{Kahf72pp|{kM7~!a&a`+ ze+?&DW5Ij>fAUqFLK68-hSGfNg6998Gpl@x!!d3d*|i4-kJw{Em#Li0Rr3Cdwuj$r zT`*oCzII5;>UVPJIUD`$go%h~#WPy<|)M?qCv`$t8Nk-9noB*Sbt zjdeD2@=HuifauwtYJT^@pi=P(^0dY!;`_$?)|#qZviMXOwD zI*s3nj(}BrJjW`Mgc}7PAHV%1Fq{K`73nMVV2TY+{{C0*x>J|9z3tn(+f37QOm z^_)c$y}TB-s#&wIBXk1maNk-)rHZK>w)3&JW;r`pj2_E-lbw+P;pXOs9UV!O>NnXR zG@S!-piH+x#_!l^ehpjhpo6=sciAbkoM1A#KG_;$J%&gPT3?-Py_A>doMFcaTiV@4 zv$L~vK7f*E%SLx6aF`_d6h}-?YYLr?zimI;o+#unZ1txzIBl;(4+eFf4M7*4*4Y z=$gKMx4_8AjN7bO>rD(O3w%chtB9xo55X&h++S#=vVQ~3CL4S|oRB?4m#v3)(HnGO zWDAF;EUPY7qebm6U`BX&c&&qj=m2UHcx5qRaB&ZpKT`7S za3^28<=I73JlD-!k#KyWm?~;FS~tqUA+y|sB*rKI`fnYns-yI~!FH@`?A<8Uu0kQ@ zwUsh7q%s{!34_7xZw`AXHJiQp0sR?B%T_6X&6uSd4JL6b6;9M{q}(FJNfrCJrEv z!fTh~VJ}}QD)Q*;btpX~_x?P8il`sO?e#hD933q!JTWox!+O9e7w>Dx*IU5f3`C$6 zT`c*4tRrQL`%&ERwzRKLuJQP6F7#7G=a(92b)B>=K+wqcb_pY$r#MD;HxU1TL0Np*@A)DdN%D(UD^Wh4Ad=4FH$+-riT! zzUzr*_F(Ns*9&foU7caLJGXZ)b5*QoE0G&q_ax-xQ0C|7K`Pk|qzcwRr~IQRMWc#! zYG_16C>-m^6oDxCor@|HMHXf;_MO=0Uf<&nz7cu=h)?hH1-AzbsJ5Q@0I~#F2Gm?! zc${WKok7UB+k1In4IsJ$guTwFo;`a4up4%M-m}ye$l-J4?7TBMxR#QW^AzNk&h+Gz z`XUI(YP-d#LXA?1?9mLJ8ruXxx00Dt%pxD?`tCu^qHnnG?Um8_{=&uOMtaPbFOR_F zgXBkyw!y*TULUK7(o*&hfq`(4%*@PM5Oy4nYcGwyYJj2Oq9QbXeSNwO{U%TRmb)uv z#T34P`FUMF0Rb9tx486*olWOZRG{86A8^BAeSkQ&t;#IjNB!ce4Uw`F{9EyMi+Wxl zAjS1A`w{N~XYUT<*~AMcXr>3+tK-MU9CC-|6cz;BLck{~1;}@$Xkx)Y^#8o_|EQ^} z8w1E8;xfmQkdOe=`v)@>hNCBB$+t5JEzz_=O zYsJ9E*4;>--Wq&yQN9038ca)^?Jb0VE!6n=XUbst&!2@!y+;s`y+#1aWUki-lRxr1 zttV8v*xR?7L<^fvoK|VGmY0{?=10%f*!5hU?nLSbyposih&S*J`|<^cMYC)vq+~H6 z7?qIb*)!>BHWrr7<&KXIC!5NRr;|D|*x zJF>kQ)C`p6+sGT1qRa`)$i8>{0s`$F9UVCf=Zk*PYK7|J*`r9;U*(=XU?zy#C9ul2 zPx#D1;>}<-%ha!efnJnS_D$-CtoqsA!=V2782ig|=_xryc}YiY60-L%=DF>u9^Kv~ zRTTa$Z0`cneXo!KIdDH2m37!0$@HAl15uameS{1Szs!DFg4&6Zg+)qRo80(FzFRjo+p~?#!F7CQsUe$?veRummm#nyf4un=G~m zh^*8uF5BDPo!>OZEVkL3tva17ovpOMML|K)KDi57ZlVU$9e?`cX(uvrejTp%C_GxN zCnG1{@j;cOT%X(kyVg^p*AQ_Q_wCyXIA&ht4s;7Hk|O=4`q3`_$r8OeLz|!MEjPc- z8yJV@Qf@)m7b?4Bin%kd$oLU_%P%SE1{Xi3acZ;Cq>?+)&SE z&>R7(8iusAw8H*dNxX>(KKC2LwANJ(d3~F9h&gHvPuw zB@fV!0;M>=pn#*H+e}3T=hLT8&)s2$$!IXUAH)}c5xAfJdGeBz;d%s`lbgF7_XHKz z5iOc|ZBtWaVSs~!6H$$bPV(-A={4ZEUt(hi)8;@;0tw0M?gJp@0b@NtcYG^1J|fCy zw(`}5KlQzH<0g8P9>5qKPO@f}4mG%66-&cg;7HpLQRn6Mf-?{SBWk!Km z?LQ_giws&Q4w|poAWPs|G8-a*C<1h31M1Xax&4XtO!*U30+#23f^#E)O+I(s)x;wt zTm|%p6UBL|L@&HImL7EuR28ltG5h=bQS|BFIB;-q0J{B0jj63Bi`oErANZEX!R347 z{yh9K|!q(6Kd1x>FJmdP+oYw^u@#;yxPz4@$|cI3sisg^|gaNiI|-nA18`K ztX6k+=KApA0~Q|MlMT;=WSOO<=wvbFJLy{ayL02?T z_kPYeou2lP(%^)R6zlM2i^DZ_OT25MPji0+1WY3g<&D06nlk$JiGWL8@*E)y9l%$V_4T7xd!pgf7FfS2$Gk`STI;23r_& ze1CAjXYoJ;V)ARCVGhUNzA@UFET#*;n9KtVMP}}SKWd?=-FsmX5}M(udU<#t8`bJB zklcdJ{c#SBii(nOFI}9BcHiE*&xfBBwR1Wr%ukKf=WVo;TTGIamVP2~f8zw`*_64_ zW__uxLJHsLCMP)sMbuELAnKfUarGoGT=?Th*}Gnxcc!K*F|rhG%%zPVybhWOfByXGemuw< zc@)!~!sqZvyI4KHB!ky}NlaB;edXrzI8xsPpc91ZHO3Rm<9xt*mlIem&n@HukyiJ; z!B!;lULC{^lLFQSI-BQA63dpn^=mW{y}I(j56&G^?;(}$!k6nxZrCUtiG#&$Q&Us# z(tR^WK?EiiO_BqY#}vK60`!uy`CfJ?ih|_kwJ!-^hEXmb)clI6?SpF;jji3*<)TsCm#Aav+!6i?k|0iw92Y zr?GV5*)Tz3A$196oiq(L)MJAj@EXJ0t5ccu7I4BhAhtup!rEr5tcrgiey2n(_Gh1Da01qD zAD2GApr8;Qk|nDu?IOJjgh1L5L#0#=DeYOHdk#TJBF}-xkEiQhm?`SdZmrnXMJfeS zOJ)!w2lU=#DiPfV)G51Y%p(}83|qAaAwQ^m`;|`Q-RYn3h!|v8QC&SfCI`@_2k(QW zK3y(X!qv5UROB||rrwI+T@qu@*SNU%m3oUGp7OT;-mnS3QdU1~bJ1kk+otx0mv_v^g&^i^EW&7mpF|u24m-FDT3u(|JhHz<8v+liSDmctRmzq2j-MB=xl|xgfxiwBPWDb6<;@NqFu+Ge7Xy=po5~;vm&?uc@rW zvr;c@Zxs*JK2-<>u67m>7cXDF#5(gi>>>nyYv)>TY;rOCy6-C|N5`bw34EvXSBi=u zH8s4D2MJEdu3x#GP_)Gq=3FEJN!Dju!@l`M-P$X2~9&%r0}%^&c6XP+49BVDgbS)S#ZE}o1WmntzjO;?+1^oUH$A% z%#0#q>EDppkYWg-0nymo+uMEyZSvx0MW50t5krWdIyU1MpZAIk&d6lceqFY-AT?Nux+R~C~ zeK;KxoKS2P`XDkgG8`l)CnuqZ5lPn|oP&#NJf9MP>5D$W?cec6-(HeL=VfPSk7i0D z&bux8m`0y(j=}@k$_Mylwo&72x{lTWc!#SKi?#LjEI{Gt-gnw4G@!%I_hteC|JCNQ zYL<ns@ex4eqrX*5|%FSL8leSUXOo<=390}&q*bQtguF(~-_^#t9F zkG?WSir6`6m%l>>5|}=y6U@;Awn{Nstn(2L9-$Rr*t2KP_<#(EJy_n`L+9n?1&M`x ztzM)VkeB9(0zPhJWP~0yttH1oK+DHR1Z<)9=5weQCeUsWK*|rV4_b8e&b`|{JW1fT zB6#4NfVF^?5|i`ZbU5BzI5gfI^(P3pFwrTdgaS}dm@3wxzflCnQv!KnwW_Z&c2p{VAaJC`XkwOB z_56sextAfhh zvw|}KoaoKjDrO*lU`v6RNnbCbA|irfV`E?43B3D-Wj$Yu&B(||_r4yEijfgHARs^@ z^7iW8`}fqs!cA=`$S)Gn9^4b)Bq(WVF@C)*7zPM)2wa)lMu_8cU4vWiZuy# zw}(jBvocPY*RC@Zd%D&kyDT+6{v)^5B+{I#tE)Pzw~nrE1aKb`gGyO>h_3>#^*YQTCB!*Ai5?>8peduOz z&BkVSRo#6?QuYD6ATo(HR8-=Kv@}TxWBLHhPz@)s@F#1OycQU zqE~u@=oAkE0>b7*VSM*$ZeE_1wVkPH2p}%+=gtOs7pYiSUTdCkfzA%#%l%b$8X6iv zJIk*AbWt8T_Mtr7-f0+l2g<02FPJ|L0INsx16HX3h-4&6N=l=-nF_ODMmw#U>FL(7 zG0wSa+lBh&`8uceMOs=~fO&9`i@2F}{4o<4mHv^^1ru~f4|!4$T4%{;6@yV^QT^zH&4c(T%feIL!&0N&1^ zQ_TXz`)H%bv9}3>?SY$`sxY#UFWNA)06CS=iE~fZZpS23Q}3$m`IPUcZXjtLZl*`i z)1K}?n*_lEnO-OMhS$~c1FIPsJfWBxwYl>w zb1B!@>P7mH5D^*PTpR-BVx4xlm6fq^EwVB(<)(}k$E?u}Om5|$#$DEPfbWMP!|0|iV?4@oKLwh!8440GB(Vx~upfPVr2 zq@C2NW*dJSKYwI$bIi8EFs|QI8+>GFY0>-|0_R=Zs3mzUG*y(9k z61No=3CX9*N-otOs>llsZXd(LQ32jo?H9gD7ulSz<9sl-K`c=LCmd|A3@FST?|yx@ z9&;HO7y2Ak=nVz1`0J;4T;ztTZ4Gj&epzZX4(V%!MmFV(U zT1{cFv9XbqzCGQZ_~~=)`WOL$hJgVIlR`)gxKGwLHlsh(n9)eMkih$6SB+K53CFD4 z?x&}E=8{Zt5_GYdAY9ECn}t2k_lq4|`+&7E^3z0?OvoL}@8%E@JMqtTnmKnl5di3ZaOsHFqfd}-^ z>Al4G;0s%V1NU>plAcW$CpVg}OdAv&jJ!j!{%4^r?pY$Rsuc^3Vu#a&;Xi8;51q?f9H{^jLJwvU%)JY)8h z@B8A0b03DWpM!N*OC# z$~=b1kU6uGF;mG*Ng|~TsU#$1EOTWp?2;)%N+BhY43QzjZ#{e0*`0I#KiBEnd)x7O zzxREH`(F22>wZ2KF0RXB-kKWg^r1IzmYFm7e9Ayx{v>1zt3+lwMJ~4ZRKB_W5?l81 z(Vmbj-~RGal;H~2482uRzXU-RzgucYsF(CkibSiWYC3@kQI6b9%fvLo9`0wZap+K= zxFvP0kI!%c<*rVK;^AIb{;{xh)$6LM7SFtYKjjAld$mEZmE7}lo(XT;bIrM|Ya?Es z);{;a!oni3Z>wo@_j|L?MV;~rQ93k*_y04aq}y!YyLGI;zkhtJPa)y0-}i)V0!MO8 zX;{95Ub@80uN8lhrLo4to*iJuprs{Ur_i27Ntv(eq3w%{`~N$INPwTu#qbf`g9oBs z^Mg^EPB-?tM4FZPuxl&My-Ia3C^+#-KTW(TQL5&};2_V%UyF`>xoDDcuvf-=k{-mh z@hDNs#oY8*l<%5b*Is&+(V&G5a{EBXNC@5Uv0`C~*__HB6foVmDS^#f&?ILg=bJ8s zkGb|cmI|-myskwlJ5K~esp=_4 zt!0iCXVSB+juH_(?zSa znO=e$E)NQFPP%t5b?DY=VNJYJ_0S=~Zz6xZ=-l?b48FT}?@sukrJ*W|5|~^vW5*7! zpUTBXboV69SpRr?DK%~C9@Df1JXZKq9X%_4hOF@bEkJ z@29_7%r!5QH#IdSmvv!%|3OJFTGL4&I`^<*%Wp#l3yZev&bOH3IZTPR##VF z+|IecRK|I6?I!g_y$s{j3(0?6{e!;!nwdg}&W_TTQ8l3#w_Htj?ftwUS)5^%sn4SL zt8<6U0iPU}(O*s-8+oEs)5I-1bNZCMyjYa($`GuO7%-oty8$O^?ce|S|6Y6sS%xef zwl9X-lv8yqyTn}X-n+-9trrq^Binu|2b&>DQ4R|13c%H7)9%=Zq6`Q=ZOo*RNl& zeXl+arIF{t{gcosL%7(56n%Z4G&(u7=Y+V4iz}^O_s5#2!Fmv83%Be^RtkFao_S*nKaLfE!6P0B?(~}!xzVR$BZn(DXOmAyy4^!Lw*RMIGe5Tsm^)SL( z%F1*dSw=N9zYlx!i$>I{P>Z=g*gt64`y)*&p*o#qYo2Gmu#S$-BuD(I*4EZM>$l=1 zD^Aybfzq#&!PutNWLBn~ZR{zfqVmS>O~DRmGLW8c_vb@rPQt&iKAaI%)E6W223TMe z?gHNqGxFt*isky;k${*dFBDg!Qm6C)IV7B0;rt!^4)9GVc%k>@ekO|At#x*QN>^Td zf>#H>`fL5GCFsOdE)NVd4`8;o?l+am)X(Om+qaMOCL!SkMn!2y`tfW{v?z*WzeTsQ z@^ABiXTUa5v9bC#V!O4s#Bxyk9PMR=JwU-`GjBV8yGkb*&Ny(2M0$lv@p1swts8>y)WqlOG=hCA9;DB zusO+k+bzaMBh4h2rNtTl+Et)CBv{s-s~BAh0b9~Ln}59P2gHq%&1~m|>muc{-xO-+ zI!o=bZ{_$QXnaRAL2|o6BOS1`?D&otwI{x_YXW+fn34<>Tt7ED%`Z&%c+K=uf#qjv zJFdS;@|1iT^{xSRr56niD=_o5SR$fcW4#|%hBEe9&k6nI+)cm5UB3#|u_fvRlqpt` z0=ggw=I7^64mNXBDqB)iNWz#L?csvb`@)lXHMqF$Cuepy`Cr~Fb-U3J+|Uaq08kT z=tuGkjlAAFaDJPXTcmeWeSKxm^l1O+&uQ2B8Bq$zmSE(kfyTtk{tGg%b`&X4Ezb1) z97QfRA7gDei@t-3#Rc5FKJS8VAHhPfYE%jbq<@^@C|8eB3&EsNDp5Ly^sGCy`_}t3 zkTh1H2di)Jp-#fsmv|1dp^OO$45X07X!|1eWd~5)O4)(Yl6#}y6c#nZ6|@(8e*2r^ z)s8Z3q6~JF?Y|-J5Nqv&&BC@+LXWMqg`1lS)Xfa0Km_unsfvSrYlb@er0dsAcT>Ng zQ!1nKZG%l(XX%*xAR0wYX@%5%><#T#SQ5H+_3Akzxpe}1+Xw6(nmqE@0f4<+U0prv z=wmj_Q02(mVuHd+v9-AH6BL1R3F4Il3p_BFhM$c7^pLjG_zR``(tlshK+1l99FF9Epez>?p;?yp8 z?mB?l=kO z_>(G^u~6}aj(LOCBnNZYsTjtpZCX44sJIo>;m_i7`diwoxO-^iAvtU3>t|Ef9m#OB z-DCGUuX4oQ8ki@ku`$n~O*MDH-umB&P=a7W*&-sc8uk{u*1HUEid`;)74L7)G^k$0 z^w&aJ3xV(O4hk3GV7;8Jw)qz?U3vy%AEEV8!mWUu8-#)(a#Lurrqn732-U(hR|=LMR%~ z$Blqsjw?&dgjQ~Vh!+&oO_CDBD#!hredppIGK05AP)y9$EVN1E6k4C&0E#&g!k$xzSsiBaes{8b&6P69`xEFBP=Z<>~(cTqC@dvd( z1ZuvIPvIg&r z$ABIfZyj(nQOhaU-5ED_7Bnj*Fd^Y+&d;_c+f7#>5+|8&*l_bePT&tKYwHKrbt_kF z5MLo^T!0sC!Z_wxR8iv{2HOl?-8(e?_qIgYR2^kb0P4(-|!sRO<)CVZy~j5rGi2fePYE~OG~ zQDQVR1tDSLJ3c;6wtS9R8KbbUFzR*(PvVdJ@Ybb*mSSjVcn8dj*TURhYH)!Z92c)z!C6YAf0K1TBB}C%8}l2ut%zWBr|B5D#7xAH(v>`T~SEw;$BMGuCumE+;=Eg!Zn^4xqBtkdG0B z>^#keqI&i9^(A98UfE#gK^AQS8AfGe_7_rPeZA_}?ow9r#0XxN=)CcS?nFE@W1ylF z_MbjC0=Ec3mM`Lj;V`4c@PgYGuKo<-mOKykWEiI6_|M2TtfNpERb-P6Q|jl72eFpC z7U%bY9VdfB#bH-T&kdMMbWBo&D5Yl>2(w7e^1I z$=&C6of>SZcwOdj{FsdO^B7U{2 zLLxH(WkS9w91@7Jnw#!nx7QDp#^kQ5yUbM=y+e2Pnl-^!uB`T5nChei{+u2qHvyxP zG8ik@iH`@teptf9DIzK=hiXUMx%+wwfQ)Jcm&|7D8v=mm7ZzL><~+}ZhFW($w9P9h zcsg3K6zNj2px=Mt9mqRaa%p2@W5Hb!Gazb<%TOyQ6j;knVjnmX<11ht z!9C4{ix@nhMqP;xxG!TJ4rU@cSPeA9BaL$ttU3NcsZa8R!`mXKyax}eh@rQ?mbtsc zb(u+#qZ~#y3b7A^#RfjG>&=0H#f>ll){s@%+Z(8xrpXOD80Jvon*ytX>z;-6_Aisi z+q7;IDia!4Iw}F=;){TTeb-|Ij#R_#UwH9tbu~2%n}~qyMI&ALDzERAo6UJiGr1)w z@kEVLlc|DhsAK3!8-uTdgTpN;A2wo7%go$)KuYR?&$M&)?YJxyBurSNL}7uMuM)#? z^83Ic0!0CPkY$2%b8|KQs4HO`fW!0Bx}EyHa7z-o=FzDU#x41$mH_EMd$aWULz1b2 zcZJHPky8>T*GOrt1j!Ht4~3+|7QK~0C0^q^$kFGKR~ai_VQ8xE9lE)v7N>Q&Hr(3B zibz|Rq!4;?xI+*P3yRli`-X>aj`*8Bu%gBC)6PG3h(bJu=X_LOF}(#`VEF6>ZylCo zO=l-N^cL)W5Wy|8-oH+~_8V05#K!WT{<5s;#4GnrQS_9@r@lVL2w2!3JFgTTCyU;W z#j*+!`v$|W?~1vW{yMouKiBAN*f;ZjZ$rb|shbLHEse%l=8X>WuJM&RAtwE^g39|# zt_X-&!jG1emV*4nKUJ6`EJ+Gw9%3u{0ytxL83iK<*R-{*C3ZTD9K|MbvwtTR>iCZl z6K?K|%~ZRhRGW)k%(=Tfuy~*~n_8Q~p}B+fIlvqWQYKkGgb@U0&|zibx9fTXU4zRMNNWGQUTk1ZUmbVQK=3HFMq_m$kB+X*;usa z&z?Pdr2u}$1F=F!88h@Q!pfR$CUU@c=CG=xH=_22`0o2hG7qR|v7e8RUk`$w772uq z(6YyjSP?)ye!6f&nf&Xx&wGzDO+@w@_{pd!#I9g^^eFeWzaMULzydC2E6_%Hs?*1YT=ELQ3G^;GpAfeToHy$qR7L!l!!? zj7l<6D@AN{R=9kI`t0&m=V3F&=sYYmdEzB{_3Bj%TiXz@<0)7TU6G~Guk~_FjnZbm zr(FJ=f2Xqx#uXAXiH-c-yFSMRqNe@=|=hd*pjFvTHr!Y^5_IWDEOvzi2oWl9B-K1ps+B8`+6DFh(6b^Uk5(x z!{z{fV*PeJUV{z|36}iTk1J9cL*z@E&~kGYqowV|U~Oqhb^7#aU)Y!^Vu{xfh5$j( z#{(C2p8tZk@5JBRK+u66FvN2PHW*Qij?=C?XSk~t8)ghv`6R1n7eR{*zJ8E0b`{R{ zJ#PmCei0{glr=Mu?NI{`zDv`}Nu`46O}gmkEOg1E=>alRZ(Uj*YjM?QWosw-(n1qR zKJ>Q8@PU1d+12(a?*1YC1+blf8}mk~J&T*P@fg9+h@67TtQZ@Yg$+&$hjK!Kr-Y$gin9+?p?>AQ< z@>n(@J)jefE7(U@fL)vhl=MOTtX7^oegrz%^_w@B$wp4Gcz`#T@%Z{$wC>bYDOeOF zlqZMlV0|Qi=9;7DXrw8shcrZdz8!#NR!T)j1C(LSoagm@)^xYaulb7X-TUk$J3HCc zC{x*Rs+?G(y5;im%xlx({-(DaK4eMQd57Kc?@RjfJ&PeP>k~UbZb*pLe$S!1<0V5) zh~f8KPbvD>>38OJ@bdH1p~$NyyiR(u^pxT741iKEOfopB$Gryeu~2QEoo_099i>b8$go=i+F3#r9Uz>p57!nJd6GwZX{{gkyQ=L)unirkY(gw&Y&=&7v6)$FKrvqN7^3Lg`46|TVm7*j|9n)ge~w0O0dI2 zX-jtB1;|K1gs-39I2e+ex;m8^jn1C1dH>vrpV@xJyIx@hP$-4=?efUZZcaN9-LX6z ze>!Y1=&&B1c%1q|^x@)u?bp`7NM=7fwJDw=qpmk^tY)7d;gE6Oh@c!T7$1!uM5)K8 z`zAiAL#tmw2P1M6e=U>ld2&F&p_@fxX64i& z?9|Vq-;%GP=;)XwqGqyT$l75}+J9lnz!AEf$wUhZ(CtPkBX+a%*c)6}yYO$!2NEy? zvprGEV9Dv7=wZ5hI`!^d3y6xZD*|LTOZ#qjgwng_gO0F%eB!fk_7u}nx7Rq;>eScQ zK_LAq-@o5X;!R6RsM)cI!Rn_^X)yO-_FwA9nZ^xAdqz8BU_2tLnw#@ZZi`wq)exHX zdHTaMCQ5vd6Fhw19dq>rqOM$_fHbmvV6tr4S<}&&J}cWR z3P%@jXH;a)t8LF1-8AKw&UC1*BQEQ+ncnYhBakh9fn~;CUu^vogDly&oTKi%u`z0V zi(wZk8@P_$F}hYelVxsBG(U}m1M9!BmREyGEGTe^JbNZOc)L9!ZuZ-Ppy`XPQBMN6 zF7iA`(Gw80eZJNU9Z>+e$WPC%@~NLhW^`og8q!4+CAA#*lO(`D___iU6S;ukV^Zep zArgz)HE$uMg|K<~Q*`wO!Cub9G*RTxshMWIVf}hbL}H24-!_&n&N)CzBE3SS#|pWg zg_Sj>N%v(OklWSUM(nAy1r?e`qajxhTMZo@vT^D7{({pT@L*zI7vUs{H*zlIQczHp zPe9kz#OZi1CvYI7lN1^)&)&W3z&^KTl+C3cG&Q}YZ|g!P8+8`id58d9(-6Pg6`;?u zul=gL-@8J;_~$hH{uKhWzf4p`RKik_%?UWgG@2M|(bqjea!cJ+!`==Fnb8LU7V=TNP{gUn89L4YLCFCj_-pMj$Fg7dh+Ors<*fI?WT*;>pp*vXUD>Y>6H$i)mB4BW!5S6tw7Y5 zhRxFfPFBHv8LXBX3`5|pgaoFI?%KKm1~=H!j_7tac6vTI_HWqc5isL(kVv}?HloU* z(bP-hc7&Mi^T9px1{peRb2CF9?}`J5?AorOO2?_CQucKI5OTa^KNXy{CXBadIQNTW%nn^TOcSf;bPb=19Z`86=lznsfLvo&hqAN$SiF*TzYzRGYH1mQCC8Ki-{Q#_v=3} z7P+Icbs~UC1<8woUTG>yAnI4dOTNyc(r0&sZ{!RkFQAZOrhs2f%qs)Rm!C}`%~^0C zwhu{=6q0RRh)??{FE8ydtw1AYktY>nCdLzXka@6qoB=BBVS`xp^lxG92m&3zz;vKu z7#uOWMujPNaGlxOJJLPfx8*3vS#ITiPlO3RTETFrX@I;rD=Sv#vcA$5cBkW%wR+$+ zI&#g0yB&TL5;s?g2d%Q8|dt`oDLCEVsY zyeGdJeXuvFSOqJ}Ik zvf_Hw zlP4BH7zkJB68Xn_R{sfWl63MydHP%QgBSPF##LO>{hzE;Vb?LCWt5hN~ zJPC266O3m0a;Cp0(ON>RMCcF%sNLHCm&^MI!g?$%jxP0n{u~NAm#W@32ezS0Xf)}= zwSjA%!Px)(lAP3VAK*N4e8rf~2!J@;!IjBM;ghGzL!gkH1Ji|H)h}OK^y#zJ^z}Ue zU3F{sH83NyLZ>n_K_*kx-`bSoe@a(Jr@;8QEEHL-T9B)vmen+tY_$JogO<9FXETEm zH)OQYsKe^?L z5d%jODFFfRLs8S93JBr7hodTR1Z)t%p1o%d?hvbtn(#FsoKjGeiR{_)6wFN2?b|Ek zHTZd#+UU@;AtNILj7GZqiG6j<2tbn%`(W-rp6LcO?#1#ZjC^3jqyC}K)u6wnrKPJ? zmx9foDsbffIZ(z7`THhXVb7YGo$VvE{lr8yh(1d@y9eJ2woiQRp$H4QpAFb4n~l#q;JwjY&~KDX{v(xE3bLs zc*2o!NP26E-~U>L+S)(u&ijQc@)S6~q6-f&zUU{zB^w%<;Py@{E6FBUh?kQs4f~Aw z)M!_@?Ta`N3O~3!C(K>~*}||G^!V|9R0_)#6cmu3lQ8du`A1sWzvd#HGZvPxO z@#E``tSEpFQTG9jjS?p*n$-_@#9l3kx5EH(pgmDi zv8S7sieXQNpc8`OlTlm4%a?@W!^hmPf(#N77iY_9LLfX07p5=H26HmmDDBIEpd*KD zgFSZv8`m1wm2mrb;jr30kxqts4KXe}1p3?TIyAdy7%U|mYk`t6sa+#_`funr=wsv%v!N%fPO45A20aD*4B1@h_A%q#U6z%d-G{(R z1Wr^s85tQkB6TH@*<@=)Xy|hMj|C100}dEW)%q90B)2M}={2qWio2(ui4*IV)G986 zoZh&z%U65vU7KrOAwSEwF3&cTOW^qW^`@(ZZ_CsTYHuC?Fzaq;5~uX)*h`W6zZsl* z4jQjv(S8r^v8BY-8dg%R?XagVUEH%e&^pJLO@Tauh3+Eg;hB@_svG@KxG* zsQXb+(2xG1!uPuGPT1SukF@+Z_xBbfV) zi(g3w!K7#bnvnuI9OXx=l-e&mjW5ZUFTi|=`iCN0|G+{0=O^bVBV`pSMsq*e|6auN z{2g9#u4VdBbQ6__MlXUVJQoyMw6%l&OMO-K=Mb=gqJgV!BZa=y^XOo7<#I8D`XgN1 zVSz?6Mq`{L*Y*EiKB#b4Pk=Pb4i4H;;{V1+X(YSLB%|+;dR@MjrlvBkgCYUSi-2A_ z#ohgTu*V-CfjKEh+Mu2fPy!1J_ukbx5i}=o5-dN)cr|)6BO+E%0uhzqf`;3e8f~Yb znU^g~SU&s5V8wVrDge9#KE;VD%2rl9ppq$+^XJb40thwN`xMIajRiHV^8mOX179E8 z`iIx=@{OP%DzeG#?HOT>OfP3lsB98IxeSH~7p$+Z57fzk#i01x@124NSHsCMG>GKT zQ>*Li>A{X59(R~5S(Q!E3`$A-H|WWi*Dts^7G4VdEKZqgN+W-In literal 0 HcmV?d00001 diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index 8c6e8e71d8..15968750d4 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -197,6 +197,7 @@ class WebhookIntegration(Integration): stream_name: Optional[str] = None, legacy: bool = False, config_options: Sequence[Tuple[str, str, OptionValidator]] = [], + dir_name: Optional[str] = None, ) -> None: if client_name is None: client_name = self.DEFAULT_CLIENT_NAME.format(name=name.title()) @@ -229,9 +230,12 @@ class WebhookIntegration(Integration): if doc is None: doc = self.DEFAULT_DOC_PATH.format(name=name, ext="md") - self.doc = doc + if dir_name is None: + dir_name = self.name + self.dir_name = dir_name + @property def url_object(self) -> URLResolver: assert self.function is not None @@ -266,7 +270,7 @@ def get_fixture_and_image_paths( integration: Integration, screenshot_config: BaseScreenshotConfig ) -> Tuple[str, str]: if isinstance(integration, WebhookIntegration): - fixture_dir = os.path.join("zerver", "webhooks", integration.name, "fixtures") + fixture_dir = os.path.join("zerver", "webhooks", integration.dir_name, "fixtures") else: fixture_dir = os.path.join("zerver", "integration_fixtures", integration.name) fixture_path = os.path.join(fixture_dir, screenshot_config.fixture_name) @@ -401,6 +405,16 @@ WEBHOOK_INTEGRATIONS: List[WebhookIntegration] = [ function="zerver.webhooks.github.view.api_github_webhook", stream_name="github", ), + WebhookIntegration( + "githubsponsors", + ["financial"], + display_name="GitHub Sponsors", + logo="images/integrations/logos/github.svg", + dir_name="github", + function="zerver.webhooks.github.view.api_github_webhook", + doc="github/githubsponsors.md", + stream_name="github", + ), WebhookIntegration("gitlab", ["version-control"], display_name="GitLab"), WebhookIntegration("gocd", ["continuous-integration"], display_name="GoCD"), WebhookIntegration("gogs", ["version-control"], stream_name="commits"), @@ -739,6 +753,7 @@ DOC_SCREENSHOT_CONFIG: Dict[str, List[BaseScreenshotConfig]] = { "gci": [ScreenshotConfig("task_abandoned_by_student.json")], "gitea": [ScreenshotConfig("pull_request__merged.json")], "github": [ScreenshotConfig("push__1_commit.json")], + "githubsponsors": [ScreenshotConfig("created.json")], "gitlab": [ScreenshotConfig("push_hook__push_local_branch_without_commits.json")], "gocd": [ScreenshotConfig("pipeline.json")], "gogs": [ScreenshotConfig("pull_request__opened.json")], @@ -836,8 +851,9 @@ DOC_SCREENSHOT_CONFIG: Dict[str, List[BaseScreenshotConfig]] = { def get_all_event_types_for_integration(integration: Integration) -> Optional[List[str]]: integration = INTEGRATIONS[integration.name] - if isinstance(integration, WebhookIntegration) and hasattr( - integration.function, "_all_event_types" - ): - return integration.function._all_event_types + if isinstance(integration, WebhookIntegration): + if integration.name == "githubsponsors": + return import_string("zerver.webhooks.github.view.SPONSORS_EVENT_TYPES") + if hasattr(integration.function, "_all_event_types"): + return integration.function._all_event_types return None diff --git a/zerver/webhooks/github/fixtures/cancelled.json b/zerver/webhooks/github/fixtures/cancelled.json new file mode 100644 index 0000000000..f2c0db266b --- /dev/null +++ b/zerver/webhooks/github/fixtures/cancelled.json @@ -0,0 +1,78 @@ +{ + "action": "cancelled", + "sponsorship": { + "node_id": "MDExOlNwb25zb3JzaGlwMQ==", + "created_at": "2019-12-20T19:24:46+00:00", + "sponsorable": { + "login": "octocat", + "id": 5, + "node_id": "MDQ6VXNlcjU=", + "avatar_url": "https://avatars2.githubusercontent.com/u/5?", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "sponsor": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + }, + "privacy_level": "private", + "tier": { + "node_id": "MDEyOlNwb25zb3JzVGllcjE=", + "created_at": "2019-12-20T19:17:05Z", + "description": "foo", + "monthly_price_in_cents": 500, + "monthly_price_in_dollars": 5, + "name": "$5 a month", + "is_one_time": false, + "is_custom_amount": false + } + }, + "sender": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + } +} diff --git a/zerver/webhooks/github/fixtures/created.json b/zerver/webhooks/github/fixtures/created.json new file mode 100644 index 0000000000..d3fb90bb19 --- /dev/null +++ b/zerver/webhooks/github/fixtures/created.json @@ -0,0 +1,78 @@ +{ + "action": "created", + "sponsorship": { + "node_id": "MDExOlNwb25zb3JzaGlwMQ==", + "created_at": "2019-12-20T19:24:46+00:00", + "sponsorable": { + "login": "octocat", + "id": 5, + "node_id": "MDQ6VXNlcjU=", + "avatar_url": "https://avatars2.githubusercontent.com/u/5?", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "sponsor": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + }, + "privacy_level": "public", + "tier": { + "node_id": "MDEyOlNwb25zb3JzVGllcjE=", + "created_at": "2019-12-20T19:17:05Z", + "description": "foo", + "monthly_price_in_cents": 500, + "monthly_price_in_dollars": 5, + "name": "$5 a month", + "is_one_time": false, + "is_custom_amount": false + } + }, + "sender": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + } +} diff --git a/zerver/webhooks/github/fixtures/edited.json b/zerver/webhooks/github/fixtures/edited.json new file mode 100644 index 0000000000..3c0e247305 --- /dev/null +++ b/zerver/webhooks/github/fixtures/edited.json @@ -0,0 +1,83 @@ +{ + "action": "edited", + "sponsorship": { + "node_id": "MDExOlNwb25zb3JzaGlwMQ==", + "created_at": "2019-12-20T19:24:46+00:00", + "sponsorable": { + "login": "octocat", + "id": 5, + "node_id": "MDQ6VXNlcjU=", + "avatar_url": "https://avatars2.githubusercontent.com/u/5?", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "sponsor": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + }, + "privacy_level": "private", + "tier": { + "node_id": "MDEyOlNwb25zb3JzVGllcjE=", + "created_at": "2019-12-20T19:17:05Z", + "description": "foo", + "monthly_price_in_cents": 500, + "monthly_price_in_dollars": 5, + "name": "$5 a month", + "is_one_time": false, + "is_custom_amount": false + } + }, + "changes": { + "privacy_level": { + "from": "public" + } + }, + "sender": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + } +} diff --git a/zerver/webhooks/github/fixtures/pending_cancellation.json b/zerver/webhooks/github/fixtures/pending_cancellation.json new file mode 100644 index 0000000000..b1186e3005 --- /dev/null +++ b/zerver/webhooks/github/fixtures/pending_cancellation.json @@ -0,0 +1,91 @@ +{ + "action": "pending_cancellation", + "sponsorship": { + "node_id": "MDExOlNwb25zb3JzaGlwMQ==", + "created_at": "2019-12-30T19:24:46+00:00", + "sponsorable": { + "login": "octocat", + "id": 5, + "node_id": "MDQ6VXNlcjU=", + "avatar_url": "https://avatars2.githubusercontent.com/u/5?", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "sponsor": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + }, + "privacy_level": "private", + "tier": { + "node_id": "MDEyOlNwb25zb3JzVGllcjE=", + "created_at": "2019-12-30T19:17:05Z", + "description": "foo", + "monthly_price_in_cents": 500, + "monthly_price_in_dollars": 5, + "name": "$5 a month", + "is_one_time": false, + "is_custom_amount": false + } + }, + "changes": { + "tier": { + "from": { + "node_id": "MDEyOlNwb25zb3JzVGllcjI=", + "created_at": "2019-12-30T19:26:26Z", + "description": "bar", + "monthly_price_in_cents": 1000, + "monthly_price_in_dollars": 10, + "name": "$10 a month" + } + } + }, + "effective_date": "2020-01-05T00:00:00+00:00", + "sender": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + } + } diff --git a/zerver/webhooks/github/fixtures/pending_tier_change.json b/zerver/webhooks/github/fixtures/pending_tier_change.json new file mode 100644 index 0000000000..2edfa305d0 --- /dev/null +++ b/zerver/webhooks/github/fixtures/pending_tier_change.json @@ -0,0 +1,91 @@ +{ + "action": "pending_tier_change", + "sponsorship": { + "node_id": "MDExOlNwb25zb3JzaGlwMQ==", + "created_at": "2019-12-20T19:24:46+00:00", + "sponsorable": { + "login": "octocat", + "id": 5, + "node_id": "MDQ6VXNlcjU=", + "avatar_url": "https://avatars2.githubusercontent.com/u/5?", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "sponsor": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + }, + "privacy_level": "private", + "tier": { + "node_id": "MDEyOlNwb25zb3JzVGllcjE=", + "created_at": "2019-12-20T19:17:05Z", + "description": "foo", + "monthly_price_in_cents": 500, + "monthly_price_in_dollars": 5, + "name": "$5 a month", + "is_one_time": false, + "is_custom_amount": false + } + }, + "changes": { + "tier": { + "from": { + "node_id": "MDEyOlNwb25zb3JzVGllcjI=", + "created_at": "2019-12-20T19:26:26Z", + "description": "bar", + "monthly_price_in_cents": 1000, + "monthly_price_in_dollars": 10, + "name": "$10 a month" + } + } + }, + "effective_date": "2019-12-30T00:00:00+00:00", + "sender": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + } +} diff --git a/zerver/webhooks/github/fixtures/tier_changed.json b/zerver/webhooks/github/fixtures/tier_changed.json new file mode 100644 index 0000000000..bb2cff587e --- /dev/null +++ b/zerver/webhooks/github/fixtures/tier_changed.json @@ -0,0 +1,90 @@ +{ + "action": "tier_changed", + "sponsorship": { + "node_id": "MDExOlNwb25zb3JzaGlwMQ==", + "created_at": "2019-12-30T19:24:46+00:00", + "sponsorable": { + "login": "octocat", + "id": 5, + "node_id": "MDQ6VXNlcjU=", + "avatar_url": "https://avatars2.githubusercontent.com/u/5?", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "sponsor": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + }, + "privacy_level": "private", + "tier": { + "node_id": "MDEyOlNwb25zb3JzVGllcjE=", + "created_at": "2019-12-30T19:17:05Z", + "description": "foo", + "monthly_price_in_cents": 500, + "monthly_price_in_dollars": 5, + "name": "$5 a month", + "is_one_time": false, + "is_custom_amount": false + } + }, + "changes": { + "tier": { + "from": { + "node_id": "MDEyOlNwb25zb3JzVGllcjI=", + "created_at": "2019-12-30T19:26:26Z", + "description": "bar", + "monthly_price_in_cents": 1000, + "monthly_price_in_dollars": 10, + "name": "$10 a month" + } + } + }, + "sender": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars2.githubusercontent.com/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": true + } +} diff --git a/zerver/webhooks/github/githubsponsors.md b/zerver/webhooks/github/githubsponsors.md new file mode 100644 index 0000000000..7816d4f471 --- /dev/null +++ b/zerver/webhooks/github/githubsponsors.md @@ -0,0 +1,22 @@ +Get GitHub Sponsors notifications in Zulip! + +1. {!create-stream.md!} + +1. {!create-an-incoming-webhook.md!} + +1. {!generate-integration-url.md!} + + You can refer to GitHub's documentation for [webhook events](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#sponsorship). + +1. Go to your profile on GitHub and click on **Sponsors dashboard**. + Select **Webhooks**. Click on **Add webhook**. GitHub may prompt + you for your password. + +1. Set **Payload URL** to the URL constructed above. Set **Content type** + to `application/json` and click **Create webhook**. + +{!congrats.md!} + +![](/static/images/integrations/githubsponsors/001.png) + +See also the [GitHub integration](/integrations/doc/github). diff --git a/zerver/webhooks/github/tests.py b/zerver/webhooks/github/tests.py index 28567a85b0..7326fd59a3 100644 --- a/zerver/webhooks/github/tests.py +++ b/zerver/webhooks/github/tests.py @@ -13,6 +13,7 @@ TOPIC_ORGANIZATION = "baxterandthehackers organization" TOPIC_BRANCH = "public-repo / changes" TOPIC_WIKI = "public-repo / wiki pages" TOPIC_DISCUSSION = "testing-gh discussion #20: Lets discuss" +TOPIC_SPONSORS = "sponsors" class GitHubWebhookTest(WebhookTestCase): @@ -591,3 +592,59 @@ A temporary team so that I can get some webhook fixtures! def test_discussion_comment_edited_msg(self) -> None: expected_message = "sbansal1999 edited a [comment](https://github.com/sbansal1999/testing-gh/discussions/20#discussioncomment-6332416) on [discussion #20](https://github.com/sbansal1999/testing-gh/discussions/20):\n\n~~~ quote\nsome random comment edited\n~~~" self.check_webhook("discussion_comment__edited", TOPIC_DISCUSSION, expected_message) + + +class GitHubSponsorsHookTests(WebhookTestCase): + STREAM_NAME = "github" + URL_TEMPLATE = "/api/v1/external/githubsponsors?stream={stream}&api_key={api_key}" + WEBHOOK_DIR_NAME = "github" + + def test_cancelled_message(self) -> None: + expected_message = "monalisa cancelled their $5 a month subscription." + self.check_webhook( + "cancelled", + TOPIC_SPONSORS, + expected_message, + ) + + def test_created_message(self) -> None: + expected_message = "monalisa subscribed for $5 a month." + self.check_webhook( + "created", + TOPIC_SPONSORS, + expected_message, + ) + + def test_pending_cancellation_message(self) -> None: + expected_message = ( + "monalisa's $5 a month subscription will be cancelled on January 05, 2020." + ) + self.check_webhook( + "pending_cancellation", + TOPIC_SPONSORS, + expected_message, + ) + + def test_pending_tier_change_message(self) -> None: + expected_message = "monalisa's subscription will change from $10 a month to $5 a month on December 30, 2019." + self.check_webhook( + "pending_tier_change", + TOPIC_SPONSORS, + expected_message, + ) + + def test_tier_changed_message(self) -> None: + expected_message = "monalisa changed their subscription from $10 a month to $5 a month." + self.check_webhook( + "tier_changed", + TOPIC_SPONSORS, + expected_message, + ) + + def test_edited_message(self) -> None: + expected_message = "monalisa changed who can see their sponsorship from public to private." + self.check_webhook( + "edited", + TOPIC_SPONSORS, + expected_message, + ) diff --git a/zerver/webhooks/github/view.py b/zerver/webhooks/github/view.py index 3459f232f9..6711ce54a0 100644 --- a/zerver/webhooks/github/view.py +++ b/zerver/webhooks/github/view.py @@ -1,4 +1,5 @@ import re +from datetime import datetime, timezone from typing import Callable, Dict, Optional from django.http import HttpRequest, HttpResponse @@ -637,6 +638,82 @@ def get_ping_body(helper: Helper) -> str: return get_setup_webhook_message("GitHub", get_sender_name(payload)) +def get_cancelled_body(helper: Helper) -> str: + payload = helper.payload + template = "{user_name} cancelled their {subscription} subscription." + return template.format( + user_name=get_sender_name(payload), + subscription=get_subscription(payload), + ).rstrip() + + +def get_created_body(helper: Helper) -> str: + payload = helper.payload + template = "{user_name} subscribed for {subscription}." + return template.format( + user_name=get_sender_name(payload), + subscription=get_subscription(payload), + ).rstrip() + + +def get_edited_body(helper: Helper) -> str: + payload = helper.payload + template = "{user_name} changed who can see their sponsorship from {prior_privacy_level} to {privacy_level}." + return template.format( + user_name=get_sender_name(payload), + prior_privacy_level=payload["changes"]["privacy_level"]["from"].tame(check_string), + privacy_level=payload["sponsorship"]["privacy_level"].tame(check_string), + ).rstrip() + + +def get_pending_cancellation_body(helper: Helper) -> str: + payload = helper.payload + template = "{user_name}'s {subscription} subscription will be cancelled on {effective_date}." + return template.format( + user_name=get_sender_name(payload), + subscription=get_subscription(payload), + effective_date=get_effective_date(payload), + ).rstrip() + + +def get_pending_tier_change_body(helper: Helper) -> str: + payload = helper.payload + template = "{user_name}'s subscription will change from {prior_subscription} to {subscription} on {effective_date}." + return template.format( + user_name=get_sender_name(payload), + prior_subscription=get_prior_subscription(payload), + subscription=get_subscription(payload), + effective_date=get_effective_date(payload), + ).rstrip() + + +def get_tier_changed_body(helper: Helper) -> str: + payload = helper.payload + template = "{user_name} changed their subscription from {prior_subscription} to {subscription}." + return template.format( + user_name=get_sender_name(payload), + prior_subscription=get_prior_subscription(payload), + subscription=get_subscription(payload), + ).rstrip() + + +def get_subscription(payload: WildValue) -> str: + return payload["sponsorship"]["tier"]["name"].tame(check_string) + + +def get_effective_date(payload: WildValue) -> str: + effective_date = payload["effective_date"].tame(check_string)[:10] + return ( + datetime.strptime(effective_date, "%Y-%m-%d") + .replace(tzinfo=timezone.utc) + .strftime("%B %d, %Y") + ) + + +def get_prior_subscription(payload: WildValue) -> str: + return payload["changes"]["tier"]["from"]["name"].tame(check_string) + + def get_repository_name(payload: WildValue) -> str: return payload["repository"]["name"].tame(check_string) @@ -728,6 +805,8 @@ def get_topic_based_on_type(payload: WildValue, event: str) -> str: number=payload["discussion"]["number"].tame(check_int), title=payload["discussion"]["title"].tame(check_string), ) + elif event in SPONSORS_EVENT_TYPES: + return "sponsors" return get_repository_name(payload) @@ -770,8 +849,23 @@ EVENT_FUNCTION_MAPPER: Dict[str, Callable[[Helper], str]] = { "team": get_team_body, "team_add": get_add_team_body, "watch": get_watch_body, + "cancelled": get_cancelled_body, + "created": get_created_body, + "edited": get_edited_body, + "pending_cancellation": get_pending_cancellation_body, + "pending_tier_change": get_pending_tier_change_body, + "tier_changed": get_tier_changed_body, } +SPONSORS_EVENT_TYPES = [ + "cancelled", + "created", + "edited", + "pending_cancellation", + "pending_tier_change", + "tier_changed", +] + IGNORED_EVENTS = [ "check_suite", "label",