From be754723157d2822f1f369b094a9db54d8ba73f4 Mon Sep 17 00:00:00 2001 From: "Wyatt J. Miller" Date: Wed, 7 Jul 2021 23:19:21 -0400 Subject: [PATCH] got rid of bins --- ...jmiller.com_OPEWNFFIWC.blacklisted_devices | 0 ...ller:matrix.wyattjmiller.com_OPEWNFFIWC.db | Bin 131072 -> 0 bytes ...yattjmiller.com_OPEWNFFIWC.ignored_devices | 2 - ...yattjmiller.com_OPEWNFFIWC.trusted_devices | 0 .../matrix/wyattjmiller/wymiller.device_id | 1 - weechat/python/autoload/_weechat.py | 1 - weechat/python/matrix.py | 710 ------ weechat/python/matrix/__init__.py | 0 .../__pycache__/__init__.cpython-39.pyc | Bin 137 -> 0 bytes .../__pycache__/bar_items.cpython-39.pyc | Bin 4467 -> 0 bytes .../matrix/__pycache__/buffer.cpython-39.pyc | Bin 43299 -> 0 bytes .../matrix/__pycache__/colors.cpython-39.pyc | Bin 24407 -> 0 bytes .../__pycache__/commands.cpython-39.pyc | Bin 38921 -> 0 bytes .../__pycache__/completion.cpython-39.pyc | Bin 6690 -> 0 bytes .../matrix/__pycache__/config.cpython-39.pyc | Bin 20858 -> 0 bytes .../matrix/__pycache__/globals.cpython-39.pyc | Bin 802 -> 0 bytes .../message_renderer.cpython-39.pyc | Bin 3335 -> 0 bytes .../matrix/__pycache__/server.cpython-39.pyc | Bin 42596 -> 0 bytes .../matrix/__pycache__/uploads.cpython-39.pyc | Bin 8744 -> 0 bytes .../matrix/__pycache__/utf.cpython-39.pyc | Bin 2661 -> 0 bytes .../matrix/__pycache__/utils.cpython-39.pyc | Bin 5274 -> 0 bytes weechat/python/matrix/_weechat.py | 260 --- weechat/python/matrix/bar_items.py | 202 -- weechat/python/matrix/buffer.py | 1810 --------------- weechat/python/matrix/colors.py | 1285 ----------- weechat/python/matrix/commands.py | 1969 ---------------- weechat/python/matrix/completion.py | 369 --- weechat/python/matrix/config.py | 916 -------- weechat/python/matrix/globals.py | 48 - weechat/python/matrix/message_renderer.py | 120 - weechat/python/matrix/server.py | 2011 ----------------- weechat/python/matrix/uploads.py | 391 ---- weechat/python/matrix/utf.py | 117 - weechat/python/matrix/utils.py | 207 -- 34 files changed, 10419 deletions(-) delete mode 100644 weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.blacklisted_devices delete mode 100644 weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.db delete mode 100644 weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.ignored_devices delete mode 100644 weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.trusted_devices delete mode 100644 weechat/matrix/wyattjmiller/wymiller.device_id delete mode 120000 weechat/python/autoload/_weechat.py delete mode 100644 weechat/python/matrix.py delete mode 100644 weechat/python/matrix/__init__.py delete mode 100644 weechat/python/matrix/__pycache__/__init__.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/bar_items.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/buffer.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/colors.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/commands.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/completion.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/config.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/globals.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/message_renderer.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/server.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/uploads.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/utf.cpython-39.pyc delete mode 100644 weechat/python/matrix/__pycache__/utils.cpython-39.pyc delete mode 100644 weechat/python/matrix/_weechat.py delete mode 100644 weechat/python/matrix/bar_items.py delete mode 100644 weechat/python/matrix/buffer.py delete mode 100644 weechat/python/matrix/colors.py delete mode 100644 weechat/python/matrix/commands.py delete mode 100644 weechat/python/matrix/completion.py delete mode 100644 weechat/python/matrix/config.py delete mode 100644 weechat/python/matrix/globals.py delete mode 100644 weechat/python/matrix/message_renderer.py delete mode 100644 weechat/python/matrix/server.py delete mode 100644 weechat/python/matrix/uploads.py delete mode 100644 weechat/python/matrix/utf.py delete mode 100644 weechat/python/matrix/utils.py diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.blacklisted_devices b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.blacklisted_devices deleted file mode 100644 index e69de29..0000000 diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.db b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.db deleted file mode 100644 index 1b82fe5c4964e436c771faec6c4341430031c7af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131072 zcmeI*Ypml~f*9s|yWTzBrP;@9jXjyTy2p;UqIUC5NPpf?kK>}sR?TDV;=31@mlwbP`0>T% zs}%k%%gdH%h9{{3>gTK2Q2t95)T|2uciu`UOHauxT7&;I+_$v`L&00KY& z2mk>f00e*l5C8%|00;m9An@}P_~$f00e*l5C8%| z00;m9An>gdxP5u+;_|)g{{IIbUYvix1O$Kp5C8%|00;m9AOHk_z(2DBx2He%UHeZq zm-jCIPCx(XfdD7NS~BO&QP-7@>^X}RTnNI#hEC-&waS&i`Rf&$%cweXMjAs+$qeD} zEnhZ{o0T{c*26wKaLXFi7Qz%Z6a=D(e2gB4AwiDi#ky*6Nro7k zQ_CU8iy5Vp%ytrWW05MevUMB@O_j;c#tvm)IP_^VUnwceD5#Jw3Ll=t&JNj!?3P+@ zsfZm$k(=V0xfP|}jRmZY(XoI*Vi<=}Pe6%Nbxr3htCL4C5NE>HWZz{Oyw5edBi5SJ zC)EKjhOoJiC9#3%CTui}r~@=vi`(G}oyNVdWV6xkcyvQ)G+yEMoXCdr%61M7Q^3Q> zB7D{gf~vC3B^>0w%6aB`yxA|KN*D*-rdZf(V@|E-#J-u;eWy8r-tI_Vn3{QShx1K= z(_1+trj+J%JaHn(B{WWlzRK^qSxeR%L4ubHSHOd??kbqEJ*}AwDnAY6V8NeG1cdWx zEUs;%QdAC~)F~{myJl67_Hi#m#RMKHti4?E?nvZ!(|HJ=qd43c+U>mKqN+8rknpO4 zSG8$!rCl!ydvCy|$JEC)Jsw8I+@$HomiO#w)o-{}sO#F=8WS5~vE1_{miR+gSkJtC zC!ONmaY$PuTO`fqm@)>rcUIKm$g|88AG2+?+~}<9>NaN|P9sqqr=oPWRs1b_e#00KY&2mk>f00e*l5C8%|00{hw2tc>b zqj$f5IA+Oi7ge8~ztmqP{ilcHs%f^*mVFu&*<9mYbutlM_Uf|#ornMS#o#|5R0HVY zAD&-=2?ziIAOHk_01yBIKmZ5;0U!VbfWR+YK)iGB;-mSG?>}JqC1Z=WH)FrCH_Pv$ zEXte5TvVq`Q9k}%CCS_ViE;aCRkQmKBu$;UOKl-q-?*w(S$-NlTivHg@%VQqNfpIK z|Hs7phbanmz&fwm~;ND<7_-_a4;IIC|ix_MG2mk>f00e*l z5C8%|00;m9AOHk_z%NSxx$|KD(d9RGqHhKz?!0A+o3V%6Z<*p|WZ~9ZrnnhMcn`kH z$2VX9zZo}p*8l&97lVH?_{W1k9{i)hKm27C0oX1O00KY&2mk>f00e*l5C8%|00;m9 zAn;EsaQoJS%R5)I+gGz&SF`tS-Foog>i++`5C8tf`P%<~Fvte~(%?JiH(&w+KmZ5; z0U!VbfB+Bx0zd!=00AKIDtEWz9N z|L;6Y@b>-x+s_ibegFT~vjlJ7|9=lUXZpcM@7Vtz{4X!u|9^f1CLjO=fB+Bx0zd!= z00AHX1b_e#00KbZS54sRUjMsSvp3%VfBAo3eE6S!$GHC|cmJc?v-2A;eai*Duv6JC!s6O@q{Uzy>A^) zBVxH^6IjP}vB&~(PO7*>a*GT_mgb?bsKmj&)mq!jZKI2OjxhRTKE*?$E9hXeM#}v> zWmQM)88g>fgIr+6TqYAQbfZNN<$`!HrX{{H!-bhhVXzbrqo_C>O&ZtXwb2Lh`OlYX zC`3h5onzH`F&-1nUZZ_o3?beSbBLWuCreq(ru$^s&nH?^ogeR7Go+j^B*yTO7T+5A z-W~Q96vlH~ElK`xLdl_xN7gVEn5fyWzOe3mS*=b^e3vDFas z&QD$~v*aY&{MO4`DfYL=L$MwX(bWm^oRw_4;zw^kEK6c&VK0y@sp%Y@uc)eQlWjqb zh?S-F#n_E2)N;$nD2HkcyYV=7M*6DWY!zz(t06C?l&4_z%x2*OmalhmAeA##TjpkA z`6E9Y`h~V93A#%4xjzcmbjTP8-}Pj6kmQCPt!Iv$^6)4fwhR@I%#zrn$pl$@#WHZn zl~Q=S#zvUxu#P+uHtlFXVS8l9RjI@sMT^B*`Wnx*^O9}ia_FBKmCxk{Epe~6AO#8!#ui)7{&2T4Otn4t@}49na3`amw}y`Kus#t(i$N2|9Vyyy55-V|jlD?%x| z%HEv#M?q^Vl+t)XQ$;_eSJQcR{!}|IpXdi)lD0mXv7v4W$C?}!+dQ>R=ooAgL*QrK zND?B4=QSzJWMhRPB@1up$@Y-$GJ_V{QE~K4tyhk;hPDv84~I$y(IHyxa0gGVVLd*; znN4=;E)Xd+Tc%VHES3vq6H-;?%(gK(5}c%1upK7VPLf%oVC>f$RH!61jw%?>rS=5z zsB|2U`-&Q2=YuNCCkIVS)j1R69WI_fk-fG#m$6HZ^!*`{lGay`h>cOpB|R@u67#x1 zs?xgHtShe`%U!;%)cJ6^IQVMMbI}6SAE~T89x<|59~a5dRg=IiVRIQKsA_oopx?U0 zM57w`3+vXG)rvKhT@e^lGgZUw`H9*4i0oT>%QeQXTuj?+SU~fGgJ`8{|AMO3+n)q` zBX4-fHS(1)qnQo38O%xquD6%H?Yc1Sj|TEOpAQ zy+#ny9ZM(vc=S=ekl5n<2yZ4ETLQX=PuebP-2IMK88W5yR2;KIXd-0pgi;n|x{}0c z7CXeY6)UYLL^DpNfqUv^1Zo~P0igzZDTl>cN)n_{+@L^OYBTl6v8od1V^6U{P{$lj zs>z64%hbl7s7W}ruu`N}UR%K1$v);gE1T|YZ{Bwa?GoO2%EITTylWQ9(X(#8I6vUq zU4%x>>NV!IjbQE?QaB&2-utpzSzCk-h6LLlOu^(jN}Bhy09h7X7NNLN3?5r03yM_z zHT?^!R&Re2)JeHL8due1A5eyMAhb?ENwwM1;`y=Zb-jfSu3LB`hnflpe_HXqBJ4K; zLN-f2nnb4>BkSdK*B-7~Vbid2r%K_h@VI8mlb}*@FthM_TS*AQxY4pUB`YSX6AGVY zPC*H8$4ZxSr=TEesJ2y>-tL!v%+h%4AV-wWkNMhMuf`B>r4E))MaY(i@c4XI8<%$L z62UpZ+BdmCthvpaJ#VgB<$Gu%m$ZZ8eOGTS-P-q?DH;T_9gEsggrYEWh5mV1kmc!K5;e{irjCyW32O`$A+zj1` zb&HS~CuwVVvpOBA4xhXd^8S@t{dFYb)dG( znV}uvQ_5|Kt&>eWVn+%*v!vueKqO6u-=WE#N>nv;hF(%*+BzQ-_M{}*vPDnDzI0n; z0@KnM-irsgbt26iing_+@@hQ75iu$~3{e&37%<9C-+QHs(R)}ZM#mLfkXi2zrLI-x zPCdla5wBXtVy#hIHo>Oo{Fo2xJlfTJXt^J;a2)Pvhn-J`8-a#{QCY2yIAo=6wrl0Z zo`pErHJqpmmU-ETv_cxOEmIxsQ1wJA;w9JY7QsZCLX!oz=ngfj5Q4&&twzGGIYzjs z!m8P{7)RlZ??}VMj84)i4^R8hQaX6WF7|M(IZ?=mr%^VO-4Q#}w}h+*7xW1IbX<@^XXibE8_ydglktwk_qBUYl17zZ&2mk>f00e*l5C8%| z00;m9AOHk@y#&7T{{M^b{(6Cc+yVh000e*l5C8%|00;m9AOHk_01)_f6S%tm5BmSV zZlw!{Fh+Fwh6TJ@~H%|JH~9$9W-`fB+Bx0zd!=00AHX z1b_e#00KbZmnHDyhj%VM3NC}T>LQdNknw5~ur5|*-HGIOha;OX$lhiTf?`o?qs?2m zHaH_*_m+U)8GU%?;s?QH6keyKCrMBoH*K+JQh3c~&8STFcu9rsb~NI#eeFfKTpjRj zM&J4I{Z}@-HQ=!~og9!{z&sQwyn`Fgp_~=j+4*S^`PN@s>bNY%jSk-)-FtDf0fHd-k2F(k_qKC>VwlV5bB*lCp`*Vydhp_Am8N8Rh^nD{ z;;NHKAgPtGJyH>GPUkc-^(W2xL`KB0llk{P``~J`4{pl#9Uk65edlVc51tq8J3Pwa z{lWiw@&4csE(ZVS;57K#ga2Xh9}oWe;NKYh`-49i?0#959BdZ|00AHX1b_e#00KY& z2mk>f00e-*uaLlZZ(n|NdFQ4(|M0`xmp{0?^SqD#;Rm-L{NT&6nD1Q2y?qn+{&n2j z&*R>^4!(60eD^x|*7M*y*TL`I1iybB{ND562WPVnzH}She(=!?!$%*yKlsPz{{Me@ zG59-!KN200e*l5C8%|00;m9AOHk_01yBIK;WMd zfjhS@FYi3Oo_~1tr~bcl_44-p>-oK_`K`Ox^E+4b_ujvrUkwS|zKZ<8)&2js-hX`Y zkoe&D&;Nl52mk>f00e*l5C8%%3Ecjjm)?RmzXk$d{P6z$fTn{7f92z5qmwmsJZ$T& zK6Zns9PxI)+*h;GE|GHEa8(75d|2C%2NXt;zW@_|0mUDq=w~qT8G?RFjwq5Ee-$!% zjAEY=ON?Zwi$rqf5h6jx=9)};cqvV2U-6A_8iWMr?>!`Pboutd(J z<}_Yx*zU;+)6l1wiK|6m zL-Vc@^$C7lS5`7r90}J@wskV>s2SU|l((HmwY$AaC)*v?2b8X}<3(G$E}7|)GH?1w zmncCHRPHOd!)s7j%DqV%JmaZz zE}z2sSG9n6jKZJ6K7#3PHyh_71n4Qx&R@Xfj*xg`S%8Xh-CVZ3N*E zsn>HHQv;#ZoS~`i#J8XKEk&$Nwhmg#n zsPCJ2KbZ|mmUD~lns zhI=B3)dW5&mN@NUylKl;srD^1*_H8pxSF&xj%yi9^=Nmd(}J~3rk=lWB!LdZQRN&2 zcDa%YN8XGLR*1HYlkcK)oiY%?HE>&AQ45#Z_e^g4{*=nmNjqLSFl$alYzd{_Pp6m9 z3jK@zAPwbsN!P@@>6Cd!^U_F!N865V5_Tt&_%_iM-)K2M$GkLjpgZnl(o`kG$qECU6r+b{s?rOV(`+WpK8Rc zpzNvR4tGnNJZEvAEA>{ctCQj5OR3cvisr*)bp9%a1mT)_{`4i@T-pc?W~>wx$>Dg~ ztjaK!ijGN(O-Om&f~3kZD)NLo3VA}T(LEWJMq+RD?4(rocGmdasL5Q;gVv;qPDyep zY({QBVOWw<9geTLyjj-!4uhl7f(c7B^wr=0 zyEWKf4EBS+Hu$^01}8L-MIZnKfB+Bx0zd!=00AHX1b_e#00RGi2tc=j%Xb(lyLT1; z9Y$X7e|jr;@D77A&-(v=d@=YZgFilh6#z^?00;m9AOHk_01yBIKmZ5;0U!VbfWWsx z;Oe&jdsnmjw{AUnaCQIx#dp6I83UOB0zd!=00AHX1b_e#00KY&2mk>f00iD3aCQA3 z^#9+00~P}TAOHk_01yBIKmZ5;0U!VbfB+EqmI#3U|F@(dK{kK@5C8%|00;m9AOHk_ z01yBIKmZ8*41ufr|1SrB@8bLeCLjO=fB+Bx0zd!=00AHX1b_e#00KbZS4QAp{Nit- zw=WhCY68PYPZ5~JVC*T5jPR#8Mv+f31bvD<9aB%S@rZhgTwVXa9Q;2Q=N~Wu0U!Vb zfB+Bx0zd!=00AHX1b_e#00Q4OfxEXJ+`4>>et7@;7a!jJql>}6{xBYJAN;+C$Oo(M zeE)+#`~JUo|NnYlx$oZnqx;{#o8HmxGPnQa*1vZ9Z{50gIe+hOUi^)VFV6qoG=2VC z-@o@r#4bOll01z1qHWeilCRU~SVj9bs+;=hDhP@;Z=NP$(Vn-Ey=hfoIS>E>KmZ5; zfuD=OPyXzO_Z|(0mp}bW&C1_JaZw#sRTzcAW|idiD{mjN22Wf3V~b`K{_)4Jgns<^ zcRxaN ztge$HKc5D_7B-n1yeOzwD>-kcj=qZcZ|WND87`)V_?*c%bKEpIK7R5O>-~F=DC+VH;kpjIcI&TQ z_?_pw^t?X2HqI;dFBkbcRE-yQ_-i%nTyx^`^);$0itKgO>C5`?@nc4rGcVM^m+RRJ zTYb5nJrB69TIbsOMP9r3 zgTM6P-lN}pa{2j>uG2<&P#w!A3a>KvV%zcMfcIanFfRvwt){#jd{t^M=KUuR?%jLz z^fcscMbj}dPywHNm3HMQX9R(4+<4xaDGCpX)D^5n<&?>?G8 zc~iN*oZqL&%kLi)ch4WSk1jv|t?QBuqb>=e^P%v?QoLDs@8uG_3HVy+-2{EDw2dhx*qX%|8y4y41^@i3z|HnPd7hh3zRK{EC)l04kA#ojR2gn4pCUJ}?|%LV z@7;U!!yjJ$^ucwRJl{h7<^Q){E{`u4zh!AWkN&j-+sAJ%i|bnW+G46VtLpXT@cexF z8WQN%-AC*X-$e3dZk{4vzPf!rvEIKrxqg0edGFDm`*W9{v)3$jQ&dqGRaY&77hk{k zGS7=aZ%OogrC+Z=KX*7 z{=a$-1QQSd0zd!=00AKIt1D3a;KjCheawTVNTdA4w)oA$fBxmR_)Wmq+Tu4sueQap zKmD^Bi#O?>A~&yp@Ok~Gt{d<_MV_^azS_;3IDh(;7SUHx-m*P(o$ar;!k;(Lt{aB0 z4(fdJ_!rsIe)8lUubF)9iprmQxwwAzHJ4{&C9jF|Y60?(?mk*zZ))1UW(7}?*DU=0 zPx9Zn_Xxwj?32AF%4-%qdbyBa6XPvQ`}swu*SGg?#?@XPsX2GN-rOnQt&1vYHrX$~ z3f<)Gr7nB@y?c+wKf3(<(RHu;JV5y6$lwbf-WQhs&MTGYweg=96+UX5{jf5_x63EvTYZ zb2Z-DB(Lj*?^bp5w5?a`=xMlWUeis#87zHeJKxw%|8f_<)=T}l9aWx~zj^o3lkZ&~ zu8aJ7h~=wp`_;SOdbx~Wj_|B(ua;eLewFUyCx8Ao?mqhQ_uf>d*DFD;=D+!Q@!flm zzW2S$pVHT*c(v^6_cvZHx2r{O`KdieecBw$*ObCJ!5`}~dLWH(V?9^!nq{Dob7 z^3|}|S4lqka<|S$>|29-kLcm$=YRfM{FRFXPjBkvDQx2bMVtIiLq5kJRxC=zW_mKIZmoXHwuv7x->krge}+Xk5zOXO%|Xr&`3tSvlU z(0Q@k*kVGOIlk_2Y40n1@9v-pQ7N0gGlPZNq)u_1Xx(1(vAMi3Ew)S66FZjj7Ek!? z34^216o?)_nv}Dn1Bxq`)gD#^3>)#xE;(56DLd@71t_pE=`cG>mtrY2M(3NIJMfLsG{`9cr--J`qJL zpcT|!3znd5f;B&uQftj}y<_wv$$=H$9~u>qOu^pTu zebUtwn-&%!HJuyKY_id9J45CvQp8V64dQzujER#T+I%sgXKg&MWJbhq5N#QfRtHc?P@1&6p!vCu3xvi znOS(HfX(2Ag{&P0+R+U%MR!D4>b<{SgnM?h-gaI%bIVbRFmy7;=s?|1`XxfCp-~Iv zk?tyGuN>7lhx;kS&x>@SH!4=(7)`(ZcpX`=x+@rYF=A)VL|JY!N4N1{$tOZo zRMU|xr?QmoGi*V#o)0PdPI0>J`edNTc9BbU(87j~uw_2BR$=8uEYtgXI+|!x#W?6% zmK1m-iz7K7PbeRm`nDn-#=A_a@)YALczANK-dJo7^)Vo}dJ$UKc6^Yd6O4*=hFa@D z>nqH0u*jaUE?N+K>KKKT3fa25AZ3d=suNV1bi}$~W;Q;Bh1i)LkeEq4vm(YL){`KA ztL1&53eaR^PbNvCBhl9&Z}WL!+HP4kZy?3U$x& ziUGG4GK=$bi3S$OjZxd(vI$0DX^)D@J91c%{JRPUm|uVtRt-< z7B($C=T3y12X_(MQx5Zq1-<5nW7e`e;QoC8Nq|$I)nIt<&(FU7JOm%`r zDQv=J;|)pXfb1l8*$}nj5+k;;Nj;!;?riBWCxo9gl|s)Ax)&sT+A)z}@@TYh^i;CB za^11^!Q%b7!c>xVK1_7;%^~N-?Lq8kw4>?=f1*rnnWuPt;zD7#wlsL?2F-b=7Nr(q z@xmj@W={DdzFo)cy5TU-(07`xo+N%!kaoPcXu0+eN|0k!Yze1%kGd40?`jl-U|dHh zqs@xfgKaG`7;cZ-q3B5-r%sV!;%H5G;Fp9Xk5d`)V0VQ!t*{}by)j$+=x9zuX`Pp4 zFyj2KSx)PHMnM$hYZJpAjJ0GqYcc0sFGikG}tNJ*!&v4Qx ztXQ4RxK3W_+gUs#Jo1QSq+Bkh)})CJabD`PIn&2JyBQst+6b6r3qEwlJBt$#Ar5h560|FU;-q*%#tNi&mScCj zLXCGzfEW;R=LSfN<7S0tKU7NWtH4YHtu%}mV+ zjBEA^G4ye`6Kd8tan#~0i*^=mWTitziWnmc!`9k;mscjl@@Y3I7&nheyD*ck*GtCh z(pny_hIzORoAJJ3H`W9pI3f-$B3bcbD(A~1+z9D$Ug9fDu?Vki&4~|B){GIc{6y&$ zc9-gq6pSQ1C)r4qdL2L4`?VHFu#>aQWNmvmd+hjdc(RhyR&^R72-$sz?VXXa8Bq~2 zUN`5O8dom1C^~z$mFHbm3*(8sE9<&8l!#b#{k$K`wHX~Lu`80140Yv_J}wBP<7RHO zOFJx%6ntI;vxu{1xhv-DDC?F?pTpD6t4m|Opt}=>3KbejHnKXxiU%*fSqT%mpAOTw zJO-`e1R@cz=aVeD;etS`8zHXqy5AfVcB~4EW5PwfjY4#LfW~ubl()z(whMc|9=Z%h zVa&|TH~2=F+UUfB_nz(AY~i`Y(cEyZk9FCXVu3y|J2d~;# z-JyZxwH2>6W0^0_ygNCSE5iYEn!(dDBRW?zRk46A!lKReI6dr+=APf1DVlFAkI#xj zHPelN4aNv{aLeH87|~Gs7;3iFLK?>2l;T~qjJ!?hEaW|`<0l*=j-)~d2*V@-E*|_2 zmszBP)r8E;&C*~XzZoiXcU>o5ASKXnI(*>p>**}|g( z(_vyz2zHgY$=2DM@nSZXONx~|RM%jx*y#B_J*{imIPc22Wh)SVzc-=jEr&$KoIu7lo2V* zFDp$nv%~t!+Ux@;FOW!|HKNM-Hil>XX@ag6dO*s@an&m3hiB6hHPj zSsor%(vF$WLVCWUx?|%p*a5EvoYnE|A|g2+Pe&cqR@He=p@VhYS@u+|tgSSStmUNj z$11Ycl~th=o^$SaC|bGom*u%GMws&=Qdbnm0{+=++Hsm`Qx^9RWTNiZ~_KsObQW*t%ryncjvW)AJ)Wxc@8U&hv) zieYlco$tW0uf00e*l S5C8%|00;m9An+?M@c#kt3r$l1 diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.ignored_devices b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.ignored_devices deleted file mode 100644 index 7b2eba9..0000000 --- a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.ignored_devices +++ /dev/null @@ -1,2 +0,0 @@ -@wymiller:matrix.wyattjmiller.com TXKANGNDNI matrix-ed25519 KGLDDeAaSEFFC4Q9Zuu2hnJIq1r2GwJNY90y4QCANkk -@wymiller:matrix.wyattjmiller.com JPNMWZPYIT matrix-ed25519 c11Z1Db3MD5KfmDAIjOYwL+vi6ORIH/iIxf+TBi6HUA diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.trusted_devices b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.trusted_devices deleted file mode 100644 index e69de29..0000000 diff --git a/weechat/matrix/wyattjmiller/wymiller.device_id b/weechat/matrix/wyattjmiller/wymiller.device_id deleted file mode 100644 index 6c7ef33..0000000 --- a/weechat/matrix/wyattjmiller/wymiller.device_id +++ /dev/null @@ -1 +0,0 @@ -OPEWNFFIWC \ No newline at end of file diff --git a/weechat/python/autoload/_weechat.py b/weechat/python/autoload/_weechat.py deleted file mode 120000 index 2dbe8e5..0000000 --- a/weechat/python/autoload/_weechat.py +++ /dev/null @@ -1 +0,0 @@ -/home/wyatt/.weechat/python/matrix/_weechat.py \ No newline at end of file diff --git a/weechat/python/matrix.py b/weechat/python/matrix.py deleted file mode 100644 index ba2eb85..0000000 --- a/weechat/python/matrix.py +++ /dev/null @@ -1,710 +0,0 @@ -# -*- coding: utf-8 -*- - -# Weechat Matrix Protocol Script -# Copyright © 2018 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import os - -# See if there is a `venv` directory next to our script, and use that if -# present. This first resolves symlinks, so this also works when we are -# loaded through a symlink (e.g. from autoload). -# See https://virtualenv.pypa.io/en/latest/userguide/#using-virtualenv-without-bin-python -# This does not support pyvenv or the python3 venv module, which do not -# create an activate_this.py: https://stackoverflow.com/questions/27462582 -activate_this = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'venv', 'bin', 'activate_this.py') -if os.path.exists(activate_this): - exec(open(activate_this).read(), {'__file__': activate_this}) - -import socket -import ssl -import textwrap -# pylint: disable=redefined-builtin -from builtins import str -from itertools import chain -# pylint: disable=unused-import -from typing import Any, AnyStr, Deque, Dict, List, Optional, Set, Text, Tuple - -import logbook -import json -import OpenSSL.crypto as crypto -from future.utils import bytes_to_native_str as n -from logbook import Logger, StreamHandler - -try: - from json.decoder import JSONDecodeError -except ImportError: - JSONDecodeError = ValueError # type: ignore - - -from nio import RemoteProtocolError, RemoteTransportError, TransportType - -from matrix import globals as G -from matrix.bar_items import ( - init_bar_items, - matrix_bar_item_buffer_modes, - matrix_bar_item_lag, - matrix_bar_item_name, - matrix_bar_item_plugin, - matrix_bar_nicklist_count, - matrix_bar_typing_notices_cb -) -from matrix.buffer import room_buffer_close_cb, room_buffer_input_cb -# Weechat searches for the registered callbacks in the scope of the main script -# file, import the callbacks here so weechat can find them. -from matrix.commands import (hook_commands, hook_key_bindings, hook_page_up, - matrix_command_buf_clear_cb, matrix_command_cb, - matrix_command_pgup_cb, matrix_invite_command_cb, - matrix_join_command_cb, matrix_kick_command_cb, - matrix_me_command_cb, matrix_part_command_cb, - matrix_redact_command_cb, matrix_topic_command_cb, - matrix_olm_command_cb, matrix_devices_command_cb, - matrix_room_command_cb, matrix_uploads_command_cb, - matrix_upload_command_cb, matrix_send_anyways_cb, - matrix_reply_command_cb, - matrix_cursor_reply_signal_cb) -from matrix.completion import (init_completion, matrix_command_completion_cb, - matrix_debug_completion_cb, - matrix_message_completion_cb, - matrix_olm_device_completion_cb, - matrix_olm_user_completion_cb, - matrix_server_command_completion_cb, - matrix_server_completion_cb, - matrix_user_completion_cb, - matrix_own_devices_completion_cb, - matrix_room_completion_cb) -from matrix.config import (MatrixConfig, config_log_category_cb, - config_log_level_cb, config_server_buffer_cb, - matrix_config_reload_cb, config_pgup_cb) -from matrix.globals import SCRIPT_NAME, SERVERS, W -from matrix.server import (MatrixServer, create_default_server, - matrix_config_server_change_cb, - matrix_config_server_read_cb, - matrix_config_server_write_cb, matrix_timer_cb, - send_cb, matrix_load_users_cb) -from matrix.utf import utf8_decode -from matrix.utils import server_buffer_prnt, server_buffer_set_title - -from matrix.uploads import UploadsBuffer, upload_cb - -try: - from urllib.parse import urlunparse -except ImportError: - from urlparse import urlunparse - -# yapf: disable -WEECHAT_SCRIPT_NAME = SCRIPT_NAME -WEECHAT_SCRIPT_DESCRIPTION = "matrix chat plugin" # type: str -WEECHAT_SCRIPT_AUTHOR = "Damir Jelić " # type: str -WEECHAT_SCRIPT_VERSION = "0.2.0" # type: str -WEECHAT_SCRIPT_LICENSE = "ISC" # type: str -# yapf: enable - - -logger = Logger("matrix-cli") - - -def print_certificate_info(buff, sock, cert): - cert_pem = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True)) - - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) - - public_key = x509.get_pubkey() - - key_type = ("RSA" if public_key.type() == crypto.TYPE_RSA else "DSA") - key_size = str(public_key.bits()) - sha256_fingerprint = x509.digest(n(b"SHA256")) - sha1_fingerprint = x509.digest(n(b"SHA1")) - signature_algorithm = x509.get_signature_algorithm() - - key_info = ("key info: {key_type} key {bits} bits, signed using " - "{algo}").format( - key_type=key_type, bits=key_size, - algo=n(signature_algorithm)) - - validity_info = (" Begins on: {before}\n" - " Expires on: {after}").format( - before=cert["notBefore"], after=cert["notAfter"]) - - rdns = chain(*cert["subject"]) - subject = ", ".join(["{}={}".format(name, value) for name, value in rdns]) - - rdns = chain(*cert["issuer"]) - issuer = ", ".join(["{}={}".format(name, value) for name, value in rdns]) - - subject = "subject: {sub}, serial number {serial}".format( - sub=subject, serial=cert["serialNumber"]) - - issuer = "issuer: {issuer}".format(issuer=issuer) - - fingerprints = (" SHA1: {}\n" - " SHA256: {}").format(n(sha1_fingerprint), - n(sha256_fingerprint)) - - wrapper = textwrap.TextWrapper( - initial_indent=" - ", subsequent_indent=" ") - - message = ("{prefix}matrix: received certificate\n" - " - certificate info:\n" - "{subject}\n" - "{issuer}\n" - "{key_info}\n" - " - period of validity:\n{validity_info}\n" - " - fingerprints:\n{fingerprints}").format( - prefix=W.prefix("network"), - subject=wrapper.fill(subject), - issuer=wrapper.fill(issuer), - key_info=wrapper.fill(key_info), - validity_info=validity_info, - fingerprints=fingerprints) - - W.prnt(buff, message) - - -def wrap_socket(server, file_descriptor): - # type: (MatrixServer, int) -> None - sock = None # type: socket.socket - - temp_socket = socket.fromfd(file_descriptor, socket.AF_INET, - socket.SOCK_STREAM) - - # fromfd() duplicates the file descriptor, we can close the one we got from - # weechat now since we use the one from our socket when calling hook_fd() - os.close(file_descriptor) - - # For python 2.7 wrap_socket() doesn't work with sockets created from an - # file descriptor because fromfd() doesn't return a wrapped socket, the bug - # was fixed for python 3, more info: https://bugs.python.org/issue13942 - # pylint: disable=protected-access,unidiomatic-typecheck - if type(temp_socket) == socket._socket.socket: - # pylint: disable=no-member - sock = socket._socketobject(_sock=temp_socket) - else: - sock = temp_socket - - # fromfd() duplicates the file descriptor but doesn't retain it's blocking - # non-blocking attribute, so mark the socket as non-blocking even though - # weechat already did that for us - sock.setblocking(False) - - message = "{prefix}matrix: Doing SSL handshake...".format( - prefix=W.prefix("network")) - W.prnt(server.server_buffer, message) - - ssl_socket = server.ssl_context.wrap_socket( - sock, do_handshake_on_connect=False, - server_hostname=server.address) # type: ssl.SSLSocket - - server.socket = ssl_socket - - try_ssl_handshake(server) - - -@utf8_decode -def ssl_fd_cb(server_name, file_descriptor): - server = SERVERS[server_name] - - if server.ssl_hook: - W.unhook(server.ssl_hook) - server.ssl_hook = None - - try_ssl_handshake(server) - - return W.WEECHAT_RC_OK - - -def try_ssl_handshake(server): - sock = server.socket - - while True: - try: - sock.do_handshake() - - cipher = sock.cipher() - cipher_message = ("{prefix}matrix: Connected using {tls}, and " - "{bit} bit {cipher} cipher suite.").format( - prefix=W.prefix("network"), - tls=cipher[1], - bit=cipher[2], - cipher=cipher[0]) - W.prnt(server.server_buffer, cipher_message) - - cert = sock.getpeercert() - if cert: - print_certificate_info(server.server_buffer, sock, cert) - - finalize_connection(server) - - return True - - except ssl.SSLWantReadError: - hook = W.hook_fd(server.socket.fileno(), 1, 0, 0, "ssl_fd_cb", - server.name) - server.ssl_hook = hook - - return False - - except ssl.SSLWantWriteError: - hook = W.hook_fd(server.socket.fileno(), 0, 1, 0, "ssl_fd_cb", - server.name) - server.ssl_hook = hook - - return False - - except (ssl.SSLError, ssl.CertificateError, socket.error) as error: - try: - str_error = error.reason if error.reason else "Unknown error" - except AttributeError: - str_error = str(error) - - message = ("{prefix}Error while doing SSL handshake" - ": {error}").format( - prefix=W.prefix("network"), error=str_error) - - server_buffer_prnt(server, message) - - server_buffer_prnt( - server, ("{prefix}matrix: disconnecting from server..." - ).format(prefix=W.prefix("network"))) - - server.disconnect() - return False - - -@utf8_decode -def receive_cb(server_name, file_descriptor): - server = SERVERS[server_name] - - while True: - try: - data = server.socket.recv(4096) - except ssl.SSLWantReadError: - break - except socket.error as error: - errno = "error" + str(error.errno) + " " if error.errno else "" - str_error = error.strerror if error.strerror else "Unknown error" - str_error = errno + str_error - - message = ("{prefix}Error while reading from " - "socket: {error}").format( - prefix=W.prefix("network"), error=str_error) - - server_buffer_prnt(server, message) - - server_buffer_prnt( - server, ("{prefix}matrix: disconnecting from server..." - ).format(prefix=W.prefix("network"))) - - server.disconnect() - - return W.WEECHAT_RC_OK - - if not data: - server_buffer_prnt( - server, - "{prefix}matrix: Error while reading from socket".format( - prefix=W.prefix("network"))) - server_buffer_prnt( - server, ("{prefix}matrix: disconnecting from server..." - ).format(prefix=W.prefix("network"))) - - server.disconnect() - break - - try: - server.client.receive(data) - except (RemoteTransportError, RemoteProtocolError) as e: - server.error(str(e)) - server.disconnect() - break - - response = server.client.next_response() - - # Check if we need to send some data back - data_to_send = server.client.data_to_send() - - if data_to_send: - server.send(data_to_send) - - if response: - server.handle_response(response) - break - - return W.WEECHAT_RC_OK - - -def finalize_connection(server): - hook = W.hook_fd( - server.socket.fileno(), - 1, - 0, - 0, - "receive_cb", - server.name - ) - - server.fd_hook = hook - server.connected = True - server.connecting = False - server.reconnect_delay = 0 - - negotiated_protocol = (server.socket.selected_alpn_protocol() or - server.socket.selected_npn_protocol()) - - if negotiated_protocol == "h2": - server.transport_type = TransportType.HTTP2 - else: - server.transport_type = TransportType.HTTP - - data = server.client.connect(server.transport_type) - server.send(data) - - server.login_info() - - -@utf8_decode -def sso_login_cb(server_name, command, return_code, out, err): - try: - server = SERVERS[server_name] - except KeyError: - message = ( - "{}{}: SSO callback ran, but no server for it was found.").format( - W.prefix("error"), SCRIPT_NAME) - W.prnt("", message) - - if return_code == W.WEECHAT_HOOK_PROCESS_ERROR: - server.error("Error while running the matrix_sso_helper. Please " - "make sure that the helper script is executable and can " - "be found in your PATH.") - server.sso_hook = None - server.disconnect() - return W.WEECHAT_RC_OK - - # The child process exited mark the hook as done. - if return_code == 0: - server.sso_hook = None - - if err != "": - W.prnt("", "stderr: %s" % err) - - if out == "": - return W.WEECHAT_RC_OK - - try: - ret = json.loads(out) - msgtype = ret.get("type") - - if msgtype == "redirectUrl": - redirect_url = "http://{}:{}".format(ret["host"], ret["port"]) - - login_url = ( - "{}/_matrix/client/r0/login/sso/redirect?redirectUrl={}" - ).format(server.homeserver.geturl(), redirect_url) - - server.info_highlight( - "The server requested a single sign-on, please open " - "this URL in your browser. Note that the " - "browser needs to run on the same host as Weechat.") - server.info_highlight(login_url) - - message = { - "server": server.name, - "url": login_url - } - W.hook_hsignal_send("matrix_sso_login", message) - - elif msgtype == "token": - token = ret["loginToken"] - server.login(token=token) - - elif msgtype == "error": - server.error("Error in the SSO helper {}".format(ret["message"])) - - else: - server.error("Unknown SSO login message received from child " - "process.") - - except JSONDecodeError: - server.error( - "Error decoding SSO login message from child process: {}".format( - out - )) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def connect_cb(data, status, gnutls_rc, sock, error, ip_address): - # pylint: disable=too-many-arguments,too-many-branches - status_value = int(status) # type: int - server = SERVERS[data] - - if status_value == W.WEECHAT_HOOK_CONNECT_OK: - file_descriptor = int(sock) # type: int - server.numeric_address = ip_address - server_buffer_set_title(server) - - wrap_socket(server, file_descriptor) - - return W.WEECHAT_RC_OK - - elif status_value == W.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND: - server.error('{address} not found'.format(address=ip_address)) - - elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND: - server.error('IP address not found') - - elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED: - server.error('Connection refused') - - elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR: - server.error('Proxy fails to establish connection to server') - - elif status_value == W.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR: - server.error('Unable to set local hostname') - - elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR: - server.error('TLS init error') - - elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR: - server.error('TLS Handshake failed') - - elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR: - server.error('Not enough memory') - - elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT: - server.error('Timeout') - - elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR: - server.error('Unable to create socket') - else: - server.error('Unexpected error: {status}'.format(status=status_value)) - - server.disconnect(reconnect=True) - return W.WEECHAT_RC_OK - - -@utf8_decode -def room_close_cb(data, buffer): - W.prnt("", - "Buffer '%s' will be closed!" % W.buffer_get_string(buffer, "name")) - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_unload_cb(): - for server in SERVERS.values(): - server.config.free() - - G.CONFIG.free() - - return W.WEECHAT_RC_OK - - -def autoconnect(servers): - for server in servers.values(): - if server.config.autoconnect: - server.connect() - - -def debug_buffer_close_cb(data, buffer): - G.CONFIG.debug_buffer = "" - return W.WEECHAT_RC_OK - - -def server_buffer_cb(server_name, buffer, input_data): - message = ("{}{}: this buffer is not a room buffer!").format( - W.prefix("error"), SCRIPT_NAME) - W.prnt(buffer, message) - return W.WEECHAT_RC_OK - - -class WeechatHandler(StreamHandler): - def __init__(self, level=logbook.NOTSET, format_string=None, filter=None, - bubble=False): - StreamHandler.__init__( - self, - object(), - level, - format_string, - None, - filter, - bubble - ) - - def write(self, item): - buf = "" - - if G.CONFIG.network.debug_buffer: - if not G.CONFIG.debug_buffer: - G.CONFIG.debug_buffer = W.buffer_new( - "Matrix Debug", "", "", "debug_buffer_close_cb", "") - - buf = G.CONFIG.debug_buffer - - W.prnt(buf, item) - - -def buffer_switch_cb(_, _signal, buffer_ptr): - """Do some buffer operations when we switch buffers. - - This function is called every time we switch a buffer. The pointer of - the new buffer is given to us by weechat. - - If it is one of our room buffers we check if the members for the room - aren't fetched and fetch them now if they aren't. - - Read receipts are send out from here as well. - """ - for server in SERVERS.values(): - if buffer_ptr == server.server_buffer: - return W.WEECHAT_RC_OK - - if buffer_ptr not in server.buffers.values(): - continue - - room_buffer = server.find_room_from_ptr(buffer_ptr) - if not room_buffer: - continue - - last_event_id = room_buffer.last_event_id - - if room_buffer.should_send_read_marker: - # A buffer may not have any events, in that case no event id is - # here returned - if last_event_id: - server.room_send_read_marker( - room_buffer.room.room_id, last_event_id) - room_buffer.last_read_event = last_event_id - - if not room_buffer.members_fetched: - room_id = room_buffer.room.room_id - server.get_joined_members(room_id) - - # The buffer is empty and we are seeing it for the first time. - # Let us fetch some messages from the room history so it doesn't feel so - # empty. - if room_buffer.first_view and room_buffer.weechat_buffer.num_lines < 10: - # TODO we may want to fetch 10 - num_lines messages here for - # consistency reasons. - server.room_get_messages(room_buffer.room.room_id) - - break - - return W.WEECHAT_RC_OK - - -def typing_notification_cb(data, signal, buffer_ptr): - """Send out typing notifications if the user is typing. - - This function is called every time the input text is changed. - It checks if we are on a buffer we own, and if we are sends out a typing - notification if the room is configured to send them out. - """ - for server in SERVERS.values(): - room_buffer = server.find_room_from_ptr(buffer_ptr) - if room_buffer: - server.room_send_typing_notice(room_buffer) - return W.WEECHAT_RC_OK - - if buffer_ptr == server.server_buffer: - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -def buffer_command_cb(data, _, command): - """Override the buffer command to allow switching buffers by short name.""" - command = command[7:].strip() - - buffer_subcommands = ["list", "add", "clear", "move", "swap", "cycle", - "merge", "unmerge", "hide", "unhide", "renumber", - "close", "notify", "localvar", "set", "get"] - - if not command: - return W.WEECHAT_RC_OK - - command_words = command.split() - - if len(command_words) >= 1: - if command_words[0] in buffer_subcommands: - return W.WEECHAT_RC_OK - - elif command_words[0].startswith("*"): - return W.WEECHAT_RC_OK - - try: - int(command_words[0]) - return W.WEECHAT_RC_OK - except ValueError: - pass - - room_buffers = [] - - for server in SERVERS.values(): - room_buffers.extend(server.room_buffers.values()) - - sorted_buffers = sorted( - room_buffers, - key=lambda b: b.weechat_buffer.number - ) - - for room_buffer in sorted_buffers: - buffer = room_buffer.weechat_buffer - - if command in buffer.short_name: - displayed = W.current_buffer() == buffer._ptr - - if displayed: - continue - - W.buffer_set(buffer._ptr, 'display', '1') - return W.WEECHAT_RC_OK_EAT - - return W.WEECHAT_RC_OK - - -if __name__ == "__main__": - if W.register(WEECHAT_SCRIPT_NAME, WEECHAT_SCRIPT_AUTHOR, - WEECHAT_SCRIPT_VERSION, WEECHAT_SCRIPT_LICENSE, - WEECHAT_SCRIPT_DESCRIPTION, 'matrix_unload_cb', ''): - - if not W.mkdir_home("matrix", 0o700): - message = ("{prefix}matrix: Error creating session " - "directory").format(prefix=W.prefix("error")) - W.prnt("", message) - - handler = WeechatHandler() - handler.format_string = "{record.channel}: {record.message}" - handler.push_application() - - # TODO if this fails we should abort and unload the script. - G.CONFIG = MatrixConfig() - G.CONFIG.read() - - hook_commands() - hook_key_bindings() - init_bar_items() - init_completion() - - W.hook_command_run("/buffer", "buffer_command_cb", "") - W.hook_signal("buffer_switch", "buffer_switch_cb", "") - W.hook_signal("input_text_changed", "typing_notification_cb", "") - - if not SERVERS: - create_default_server(G.CONFIG) - - autoconnect(SERVERS) diff --git a/weechat/python/matrix/__init__.py b/weechat/python/matrix/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/weechat/python/matrix/__pycache__/__init__.cpython-39.pyc b/weechat/python/matrix/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index ac03e49d66edb997e2b69410ad43b051733bb1df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137 zcmYe~<>g`k0)yg(2_X70h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2BxKO;XkRlmG4 zv7|&_uRJw1IU}(|zo4=tBR@|+H?gEBvqC>UJ~J<~BtBlRpz;=nO>TZlX-=vg$ehnW G%m4sEOCEgy diff --git a/weechat/python/matrix/__pycache__/bar_items.cpython-39.pyc b/weechat/python/matrix/__pycache__/bar_items.cpython-39.pyc deleted file mode 100644 index b8c8608f913f72399b7ee915f316204d9c422faf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4467 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–#@$Aqa_w1D*|N#buofyk6FO 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 diff --git a/weechat/python/matrix/__pycache__/commands.cpython-39.pyc b/weechat/python/matrix/__pycache__/commands.cpython-39.pyc deleted file mode 100644 index 4a9534ec8116ef413242729af2ee061a2d7fa192..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38921 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 diff --git a/weechat/python/matrix/__pycache__/completion.cpython-39.pyc b/weechat/python/matrix/__pycache__/completion.cpython-39.pyc deleted file mode 100644 index cae2c27d49495d748c459b5e1ed6eabc76c624c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6690 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 rW|9lzZN%_`l6M`v=>d diff --git a/weechat/python/matrix/__pycache__/message_renderer.cpython-39.pyc b/weechat/python/matrix/__pycache__/message_renderer.cpython-39.pyc deleted file mode 100644 index 93eed0b782619797093278d5e79c4008d712e4ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3335 zcma)8S##7z5Y~*2)vnhEmpM`)3M9Z1XHAkSQsr_{2~_2Hzz-0L%92EpHDg;?hcnU| ztlfRWul^6-NB^aH%@hA11-ZIMhlK-G(XK{4Gd)Lt^L6)lv$Guyo?Gek*J}%!_B&RZ zj{%jh;7z}Sf@uSd5k^B2(E%kW>!BVQ0|R7(nV}h3153$P*oy3dO|)+{)?)U)#%$hx zp$|H&{ZQ+5@>};4HVnD5kqAfdm~j!rn@;3qB6#jZJWah#o;pG7Jm%cr^0NL*SYPWA zxiE|aKVjSrgNzF=OrcHxdM9{EihlAK8rt6^0_Ms%^Kn@7sSVnPR;~>Q7)2Qw=-gnG z>HFFXGB80t$4qAJ6R25GTU171tp&9<>nJ@NjeKz~Y0Js zDRx@*bU{AD&MJA9834vbX+2N~-qU&X2)})=RS}hHsCyZ@O}Rk9ZjUM5rn*F3S3UkfP$MP z?pBuVhTquylDbzz~isB4N(o{=*3}T8b(ey4k zP4aGOr|R@o2X?~Kh8NdUR<43jS&6oc&8a!D#+JA=wqiQgCM~dg=?%MWWp`_Amv%3` z-EJq@aJ<;zu`l*^z=a(|pfAqBf?^Sh3XH{C!!2Ri35U`8NnG0E0*<^0h19qCo;0?+ zbW74;S6sq28^LseW$y-RzOfRX&Jv7E5p+#A2(5mvjg|s%u>|nMpf@}dPZM7SC2j-2 zT2ycXNhWkm59w5UsEy(6k-VJI@f?vPQ57EhJze1O$o7UvBH%<6$_|jDWIOaKo97D8 z0U%vPjdoo>$%j_y5i2lhYD0nMb%;&#xtbFyF#pGtniY3o5SX%nfo2Su;s6=1PY5&0 zz+;wy$11-q2tcT5ADC!1nA^^sniqmI&qoUIT^?t(;rZDmNE%+6#4FB~Q6WvPK-{+q zS!(%{MqpYUg~?_a)9x3k1%{V1ukWomBcI191iXe`HEUy|+qhI1#OSgZK*S6bvR!XLTE$x8I(E)M0q67y%Gdxg&&#}%=VDizt=XcP^q)CO z|DrAW4>d4A*~r2_teJeiJgRDpk9o0rP_Aj$g^D^S4bMqMq7ROdN1~MshPYZBKtI1Z zg6z9JalnP3*IN(qYzjw3Ey&l|R*)*19uaAv@)0RKvJcChv}_*v>>f|>Bj}_mgXtCN z`zKW1;2x^harX*T-o-t|G4lZVW#u2$R+fY>BhE11j+19#!rl(==d;KyKZ!G7o+4MP ztcV-1P=5JQsoj_Z2Rfr{GV&)qDoG}ZFK&S{RJx^_i#p5u%_M~5N2lxOqlOPO zUFnfJeIg5Fs;$Ws-{l#AS` zAcM65Qv(E@w++AVU&B*vTe6BEEoC;3|XJTpHK`am-ZE zH|$a->FXXFqCKUDbc(G&*kL`gcKn}{w(G`T#9dc*TsKlTZIHXJ``ge9%bu3&vc$*c zH1o2+kJMFD-6h0(u!{H)i)&bXip6JG+{WTdEHFtlHdq6%ZX}q|QXC35CrU}DMM>9! zcNSzDzUSa;z_$zEHYIAzT5m>R`oJx346~tt?F+P2>T!@1_n+2gn5@H{zqkfkD(a?1 i610-_r*LU5E=0!{oqAu=UM+yQgH4JO2JI5{)Bgqazhx)@ diff --git a/weechat/python/matrix/__pycache__/server.cpython-39.pyc b/weechat/python/matrix/__pycache__/server.cpython-39.pyc deleted file mode 100644 index 0ffeb920e8b66d69573f5fb59ed439af830ca2df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42596 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 diff --git a/weechat/python/matrix/__pycache__/uploads.cpython-39.pyc b/weechat/python/matrix/__pycache__/uploads.cpython-39.pyc deleted file mode 100644 index 0065fd93af6c7bcf3a460ff8fbb83847c58dc96c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8744 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 Hq@n*G*jwBS diff --git a/weechat/python/matrix/__pycache__/utf.cpython-39.pyc b/weechat/python/matrix/__pycache__/utf.cpython-39.pyc deleted file mode 100644 index 992a4dcb4c23128937701aac8e61fb5149b04c50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2661 zcmc&#y>A;g6esUXCs~peCrHvFX|F|4RBa;lYse4;K~o^;P%Ul|;D%mKM@l~VPN$Mo z9Sh2&0P)nZvki3d+CPVDr~C_D+V@DymW{S+d3Yp`e0+Sr_rR&3KTm+R@O(>$vfPl{Y|krr~$b+kV`R9tFTDsf7+e<0GCeCy%?M~B}c->~4i z_FX!g<Jp{6s=lB`Ij1Z6$Ri;-vo>nRa!82;?` z_o|Zj4-QkM`n>~tS$}q@_Nr;WOqIxA_UCHUn;q&fN%Co~lB9?C8Aa&g)@A*AYo1Gr zqN&y-ci`s@c|%9m1mP=Zi5_qVm+Xe@KG6F>#%@PMktJEt97npMztLYP9zCKH3c+#j zi0%>v|0@{97M$w3y3MD?m#Rtt!$SjHcNjN|BTbqxr{GhU1CBJ+O93`{MU8==z*tr{v+s?WI~SVlAeGOt=YtT zOLFWkof9;qw`oZK%y3CE<_h9OnRCM#Z|}9Ld1C2a~0D3+enhKV)Fv$%_RA0o)&gT zK#e8V;CFErVP_0`(V>Y2R)hyYMu(7wUf2xlW;LU*yl$At7*UvU5+hKNBgnHiWJFDk z3Rm&5 zi0r(E^x#L431EWf_kg%q&0#aY)D!0QZhIOIZoaOI)YjXnPIXD%dY zYeU@;*I{4WfDWF8a1F&e!$f(_7GUu`u*d*9;x%a7S=_;Ll-NRaQ5Mda1?t>&#HTO_ zd@`)SPjF^)XXEq$R;}b9pzTng!!NlPU45P+L-@fms_4oQHy}Zv2Ah1xH5b-CT*BjF zI6V0KTKQ-e6{s5Uh9_iY%B$J0!~Tz(`*sh{pE`qu4>~@02oG2zm-$qxbP6ekA*KTh zTVTwII<9!?aK^P;rZZz#?ZeYsN`Vyt?274@ZUnfAZ7ek}fXr(`pk(0Z0o?@Jp+?UC zZ3clM*KLSC36#c;`oz_RDlq2^jJz4BJ@+`Dt^t zmku+lrt#m_#+DAM;RM)BoiWj9d|}~T;4w23%;6(j<{sk{A~BbmPUv=M)45G|oKC&- JLAVn({{~2JU~d2b diff --git a/weechat/python/matrix/__pycache__/utils.cpython-39.pyc b/weechat/python/matrix/__pycache__/utils.cpython-39.pyc deleted file mode 100644 index fc140cccfd65b22249117cff776643fa662868d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5274 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 str - # yapf: disable - escape_codes = [] - reset_code = "0" - - def make_fg_color(color_code): - return "38;5;{}".format(color_code) - - def make_bg_color(color_code): - return "48;5;{}".format(color_code) - - attributes = { - "bold": "1", - "-bold": "21", - "reverse": "27", - "-reverse": "21", - "italic": "3", - "-italic": "23", - "underline": "4", - "-underline": "24", - "reset": "0", - "resetcolor": "39" - } - - short_attributes = { - "*": "1", - "!": "27", - "/": "3", - "_": "4" - } - - colors = color_name.split(",", 2) - - fg_color = colors.pop(0) - - bg_color = colors.pop(0) if colors else "" - - if fg_color in attributes: - escape_codes.append(attributes[fg_color]) - else: - chars = list(fg_color) - - for char in chars: - if char in short_attributes: - escape_codes.append(short_attributes[char]) - elif char == "|": - reset_code = "" - else: - break - - stripped_color = fg_color.lstrip("*_|/!") - - if stripped_color in WEECHAT_BASE_COLORS: - escape_codes.append( - make_fg_color(WEECHAT_BASE_COLORS[stripped_color])) - - elif stripped_color.isdigit(): - num_color = int(stripped_color) - if 0 <= num_color < 256: - escape_codes.append(make_fg_color(stripped_color)) - - if bg_color in WEECHAT_BASE_COLORS: - escape_codes.append(make_bg_color(WEECHAT_BASE_COLORS[bg_color])) - else: - if bg_color.isdigit(): - num_color = int(bg_color) - if 0 <= num_color < 256: - escape_codes.append(make_bg_color(bg_color)) - - escape_string = "\033[{}{}m".format(reset_code, ";".join(escape_codes)) - - return escape_string - - -def prefix(prefix_string): - prefix_to_symbol = { - "error": "=!=", - "network": "--", - "action": "*", - "join": "-->", - "quit": "<--" - } - - if prefix_string in prefix_to_symbol: - return prefix_to_symbol[prefix] - - return "" - - -def prnt(_, message): - print(message) - - -def prnt_date_tags(_, date, tags_string, data): - message = "{} {} [{}]".format( - datetime.datetime.fromtimestamp(date), - data, - tags_string - ) - print(message) - - -def config_search_section(*_, **__): - pass - - -def config_new_option(*_, **__): - pass - - -def mkdir_home(*_, **__): - return True - - -def info_get(info, *_): - if info == "nick_color_name": - return random.choice(list(WEECHAT_BASE_COLORS.keys())) - - return "" - - -def buffer_new(*_, **__): - return "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(8) - ) - - -def buffer_set(*_, **__): - return - - -def buffer_get_string(_ptr, property): - if property == "localvar_type": - return "channel" - return "" - - -def buffer_get_integer(_ptr, property): - return 0 - - -def current_buffer(): - return 1 - - -def nicklist_add_group(*_, **__): - return - - -def nicklist_add_nick(*_, **__): - return - - -def nicklist_remove_nick(*_, **__): - return - - -def nicklist_search_nick(*args, **kwargs): - return buffer_new(args, kwargs) - - -def string_remove_color(message, _): - return message diff --git a/weechat/python/matrix/bar_items.py b/weechat/python/matrix/bar_items.py deleted file mode 100644 index 23e6fc9..0000000 --- a/weechat/python/matrix/bar_items.py +++ /dev/null @@ -1,202 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -from . import globals as G -from .globals import SERVERS, W -from .utf import utf8_decode - - -@utf8_decode -def matrix_bar_item_plugin(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - return "matrix{color}/{color_fg}{name}".format( - color=W.color("bar_delim"), - color_fg=W.color("bar_fg"), - name=server.name, - ) - - ptr_plugin = W.buffer_get_pointer(buffer, "plugin") - name = W.plugin_get_name(ptr_plugin) - - return name - - -@utf8_decode -def matrix_bar_item_name(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values(): - color = ( - "status_name_ssl" - if server.ssl_context.check_hostname - else "status_name" - ) - - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - - return "{color}{name}".format( - color=W.color(color), name=room.display_name - ) - - if buffer == server.server_buffer: - color = ( - "status_name_ssl" - if server.ssl_context.check_hostname - else "status_name" - ) - - return "{color}server{del_color}[{color}{name}{del_color}]".format( - color=W.color(color), - del_color=W.color("bar_delim"), - name=server.name, - ) - - name = W.buffer_get_string(buffer, "name") - - return "{}{}".format(W.color("status_name"), name) - - -@utf8_decode -def matrix_bar_item_lag(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - if server.lag >= G.CONFIG.network.lag_min_show: - color = W.color("irc.color.item_lag_counting") - if server.lag_done: - color = W.color("irc.color.item_lag_finished") - - lag = "{0:.3f}" if round(server.lag) < 1000 else "{0:.0f}" - lag_string = "Lag: {color}{lag}{ncolor}".format( - lag=lag.format((server.lag / 1000)), - color=color, - ncolor=W.color("reset"), - ) - return lag_string - return "" - - return "" - - -@utf8_decode -def matrix_bar_item_buffer_modes(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - modes = [] - - if room.encrypted: - modes.append(G.CONFIG.look.encrypted_room_sign) - - if (server.client - and server.client.room_contains_unverified(room.room_id)): - modes.append(G.CONFIG.look.encryption_warning_sign) - - if not server.connected or not server.client.logged_in: - modes.append(G.CONFIG.look.disconnect_sign) - - if room_buffer.backlog_pending or server.busy: - modes.append(G.CONFIG.look.busy_sign) - - return "".join(modes) - - return "" - - -@utf8_decode -def matrix_bar_nicklist_count(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - color = W.color("status_nicklist_count") - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - return "{}{}".format(color, room.member_count) - - nicklist_enabled = bool(W.buffer_get_integer(buffer, "nicklist")) - - if nicklist_enabled: - nick_count = W.buffer_get_integer(buffer, "nicklist_visible_count") - return "{}{}".format(color, nick_count) - - return "" - - -@utf8_decode -def matrix_bar_typing_notices_cb(data, item, window, buffer, extra_info): - """Update a status bar item showing users currently typing. - This function is called by weechat every time a buffer is switched or - W.bar_item_update() is explicitly called. The bar item shows - currently typing users for the current buffer.""" - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - - if room.typing_users: - nicks = [] - - for user_id in room.typing_users: - if user_id == room.own_user_id: - continue - - nick = room_buffer.displayed_nicks.get(user_id, user_id) - nicks.append(nick) - - if not nicks: - return "" - - msg = "{}{}".format( - G.CONFIG.look.bar_item_typing_notice_prefix, - ", ".join(sorted(nicks)) - ) - - max_len = G.CONFIG.look.max_typing_notice_item_length - if len(msg) > max_len: - msg[:max_len - 3] + "..." - - return msg - - return "" - - return "" - - -def init_bar_items(): - W.bar_item_new("(extra)buffer_plugin", "matrix_bar_item_plugin", "") - W.bar_item_new("(extra)buffer_name", "matrix_bar_item_name", "") - W.bar_item_new("(extra)lag", "matrix_bar_item_lag", "") - W.bar_item_new( - "(extra)buffer_nicklist_count", - "matrix_bar_nicklist_count", - "" - ) - W.bar_item_new( - "(extra)matrix_typing_notice", - "matrix_bar_typing_notices_cb", - "" - ) - W.bar_item_new("(extra)buffer_modes", "matrix_bar_item_buffer_modes", "") - W.bar_item_new("(extra)matrix_modes", "matrix_bar_item_buffer_modes", "") diff --git a/weechat/python/matrix/buffer.py b/weechat/python/matrix/buffer.py deleted file mode 100644 index 7cc305e..0000000 --- a/weechat/python/matrix/buffer.py +++ /dev/null @@ -1,1810 +0,0 @@ -# -*- coding: utf-8 -*- - -# Weechat Matrix Protocol Script -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import time -import attr -import pprint -from builtins import super -from functools import partial -from collections import deque -from typing import Dict, List, NamedTuple, Optional, Set -from uuid import UUID - -from nio import ( - Api, - PowerLevelsEvent, - RedactedEvent, - RedactionEvent, - RoomAliasEvent, - RoomEncryptionEvent, - RoomMemberEvent, - RoomMessage, - RoomMessageEmote, - RoomMessageMedia, - RoomEncryptedMedia, - RoomMessageNotice, - RoomMessageText, - RoomMessageUnknown, - RoomNameEvent, - RoomTopicEvent, - MegolmEvent, - Event, - OlmTrustError, - UnknownEvent, - FullyReadEvent, - BadEvent, - UnknownBadEvent, -) - -from . import globals as G -from .colors import Formatted -from .config import RedactType, NewChannelPosition -from .globals import SCRIPT_NAME, SERVERS, W, TYPING_NOTICE_TIMEOUT -from .utf import utf8_decode -from .message_renderer import Render -from .utils import ( - server_ts_to_weechat, - shorten_sender, - string_strikethrough, - color_pair, -) - - -@attr.s -class OwnMessages(object): - sender = attr.ib(type=str) - age = attr.ib(type=int) - event_id = attr.ib(type=str) - uuid = attr.ib(type=str) - room_id = attr.ib(type=str) - formatted_message = attr.ib(type=Formatted) - - -class OwnMessage(OwnMessages): - pass - - -class OwnAction(OwnMessage): - pass - - -@utf8_decode -def room_buffer_input_cb(server_name, buffer, input_data): - server = SERVERS[server_name] - room_buffer = server.find_room_from_ptr(buffer) - - if not room_buffer: - # TODO log error - return W.WEECHAT_RC_ERROR - - if not server.connected: - room_buffer.error("You are not connected to the server") - return W.WEECHAT_RC_ERROR - - if not server.client.logged_in: - room_buffer.error("You are not logged in.") - return W.WEECHAT_RC_ERROR - - data = W.string_input_for_buffer(input_data) - - if not data: - data = input_data - - formatted_data = Formatted.from_input_line(data) - - try: - server.room_send_message(room_buffer, formatted_data, "m.text") - room_buffer.last_message = None - except OlmTrustError as e: - if (G.CONFIG.network.resending_ignores_devices - and room_buffer.last_message): - room_buffer.error("Ignoring unverified devices.") - - if (room_buffer.last_message.to_weechat() == - formatted_data.to_weechat()): - server.room_send_message(room_buffer, formatted_data, "m.text", - ignore_unverified_devices=True) - room_buffer.last_message = None - else: - # If the item is a normal user message store it in the - # buffer to enable the send-anyways functionality. - room_buffer.error("Untrusted devices found in room: {}".format(e)) - room_buffer.last_message = formatted_data - - return W.WEECHAT_RC_OK - - -@utf8_decode -def room_buffer_close_cb(server_name, buffer): - server = SERVERS[server_name] - room_buffer = server.find_room_from_ptr(buffer) - - if room_buffer: - room_id = room_buffer.room.room_id - server.buffers.pop(room_id, None) - server.room_buffers.pop(room_id, None) - - return W.WEECHAT_RC_OK - - -class WeechatUser(object): - def __init__(self, nick, host=None, prefix="", join_time=None): - # type: (str, str, str, int) -> None - self.nick = nick - self.host = host - self.prefix = prefix - self.color = W.info_get("nick_color_name", nick) - self.join_time = join_time or time.time() - self.speaking_time = None # type: Optional[int] - - def update_speaking_time(self, new_time=None): - self.speaking_time = new_time or time.time() - - @property - def joined_recently(self): - # TODO make the delay configurable - delay = 30 - limit = time.time() - (delay * 60) - return self.join_time < limit - - @property - def spoken_recently(self): - if not self.speaking_time: - return False - - # TODO make the delay configurable - delay = 5 - limit = time.time() - (delay * 60) - return self.speaking_time < limit - - -class RoomUser(WeechatUser): - def __init__(self, nick, user_id=None, power_level=0, join_time=None): - # type: (str, str, int, int) -> None - prefix = self._get_prefix(power_level) - super().__init__(nick, user_id, prefix, join_time) - - @property - def power_level(self): - # This shouldn't be used since it's a lossy function. It's only here - # for the setter - if self.prefix == "&": - return 100 - if self.prefix == "@": - return 50 - if self.prefix == "+": - return 10 - return 0 - - @power_level.setter - def power_level(self, level): - self.prefix = self._get_prefix(level) - - @staticmethod - def _get_prefix(power_level): - # type: (int) -> str - if power_level >= 100: - return "&" - if power_level >= 50: - return "@" - if power_level > 0: - return "+" - return "" - - -class WeechatChannelBuffer(object): - tags = { - "message": [SCRIPT_NAME + "_message", "notify_message", "log1"], - "message_private": [ - SCRIPT_NAME + "_message", - "notify_private", - "log1" - ], - "self_message": [ - SCRIPT_NAME + "_message", - "notify_none", - "no_highlight", - "self_msg", - "log1", - ], - "action": [ - SCRIPT_NAME + "_message", - SCRIPT_NAME + "_action", - "notify_message", - "log1", - ], - "action_private": [ - SCRIPT_NAME + "_message", - SCRIPT_NAME + "_action", - "notify_private", - "log1", - ], - "notice": [SCRIPT_NAME + "_notice", "notify_message", "log1"], - "old_message": [ - SCRIPT_NAME + "_message", - "notify_message", - "no_log", - "no_highlight", - ], - "join": [SCRIPT_NAME + "_join", "log4"], - "part": [SCRIPT_NAME + "_leave", "log4"], - "kick": [SCRIPT_NAME + "_kick", "log4"], - "invite": [SCRIPT_NAME + "_invite", "log4"], - "topic": [SCRIPT_NAME + "_topic", "log3"], - } - - membership_messages = { - "join": "has joined", - "part": "has left", - "kick": "has been kicked from", - "invite": "has been invited to", - } - - class Line(object): - def __init__(self, pointer): - self._ptr = pointer - - @property - def _hdata(self): - return W.hdata_get("line_data") - - @property - def prefix(self): - return W.hdata_string(self._hdata, self._ptr, "prefix") - - @prefix.setter - def prefix(self, new_prefix): - new_data = {"prefix": new_prefix} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def message(self): - return W.hdata_string(self._hdata, self._ptr, "message") - - @message.setter - def message(self, new_message): - # type: (str) -> None - new_data = {"message": new_message} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def tags(self): - tags_count = W.hdata_get_var_array_size( - self._hdata, self._ptr, "tags_array" - ) - - tags = [ - W.hdata_string(self._hdata, self._ptr, "%d|tags_array" % i) - for i in range(tags_count) - ] - return tags - - @tags.setter - def tags(self, new_tags): - # type: (List[str]) -> None - new_data = {"tags_array": ",".join(new_tags)} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def date(self): - # type: () -> int - return W.hdata_time(self._hdata, self._ptr, "date") - - @date.setter - def date(self, new_date): - # type: (int) -> None - new_data = {"date": str(new_date)} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def date_printed(self): - # type: () -> int - return W.hdata_time(self._hdata, self._ptr, "date_printed") - - @date_printed.setter - def date_printed(self, new_date): - # type: (int) -> None - new_data = {"date_printed": str(new_date)} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def highlight(self): - # type: () -> bool - return bool(W.hdata_char(self._hdata, self._ptr, "highlight")) - - def update( - self, - date=None, - date_printed=None, - tags=None, - prefix=None, - message=None, - highlight=None, - ): - new_data = {} - - if date is not None: - new_data["date"] = str(date) - if date_printed is not None: - new_data["date_printed"] = str(date_printed) - if tags is not None: - new_data["tags_array"] = ",".join(tags) - if prefix is not None: - new_data["prefix"] = prefix - if message is not None: - new_data["message"] = message - if highlight is not None: - new_data["highlight"] = highlight - - if new_data: - W.hdata_update(self._hdata, self._ptr, new_data) - - def __init__(self, name, server_name, user): - # type: (str, str, str) -> None - - # Previous buffer num before create - cur_num = W.buffer_get_integer(W.current_buffer(), "number") - self._ptr = W.buffer_new( - name, - "room_buffer_input_cb", - server_name, - "room_buffer_close_cb", - server_name, - ) - - new_channel_position = G.CONFIG.look.new_channel_position - if new_channel_position == NewChannelPosition.NONE: - pass - elif new_channel_position == NewChannelPosition.NEXT: - self.number = cur_num + 1 - elif new_channel_position == NewChannelPosition.NEAR_SERVER: - server = G.SERVERS[server_name] - last_similar_buffer_num = max( - (room.weechat_buffer.number for room - in server.room_buffers.values()), - default=W.buffer_get_integer(server.server_buffer, "number") - ) - self.number = last_similar_buffer_num + 1 - - self.name = "" - self.users = {} # type: Dict[str, WeechatUser] - self.smart_filtered_nicks = set() # type: Set[str] - - self.topic_author = "" - self.topic_date = None - - W.buffer_set(self._ptr, "localvar_set_type", "private") - W.buffer_set(self._ptr, "type", "formatted") - - W.buffer_set(self._ptr, "localvar_set_channel", name) - - W.buffer_set(self._ptr, "localvar_set_nick", user) - - W.buffer_set(self._ptr, "localvar_set_server", server_name) - - W.nicklist_add_group( - self._ptr, "", "000|o", "weechat.color.nicklist_group", 1 - ) - W.nicklist_add_group( - self._ptr, "", "001|h", "weechat.color.nicklist_group", 1 - ) - W.nicklist_add_group( - self._ptr, "", "002|v", "weechat.color.nicklist_group", 1 - ) - W.nicklist_add_group( - self._ptr, "", "999|...", "weechat.color.nicklist_group", 1 - ) - - W.buffer_set(self._ptr, "nicklist", "1") - W.buffer_set(self._ptr, "nicklist_display_groups", "0") - - W.buffer_set(self._ptr, "highlight_words", user) - - # TODO make this configurable - W.buffer_set( - self._ptr, "highlight_tags_restrict", SCRIPT_NAME + "_message" - ) - - @property - def _hdata(self): - return W.hdata_get("buffer") - - def add_smart_filtered_nick(self, nick): - self.smart_filtered_nicks.add(nick) - - def remove_smart_filtered_nick(self, nick): - self.smart_filtered_nicks.discard(nick) - - def unmask_smart_filtered_nick(self, nick): - if nick not in self.smart_filtered_nicks: - return - - for line in self.lines: - filtered = False - join = False - tags = line.tags - - if "nick_{}".format(nick) not in tags: - continue - - if SCRIPT_NAME + "_smart_filter" in tags: - filtered = True - elif SCRIPT_NAME + "_join" in tags: - join = True - - if filtered: - tags.remove(SCRIPT_NAME + "_smart_filter") - line.tags = tags - - if join: - break - - self.remove_smart_filtered_nick(nick) - - @property - def input(self): - # type: () -> str - """Get the bar item input text of the buffer.""" - return W.buffer_get_string(self._ptr, "input") - - @property - def num_lines(self): - own_lines = W.hdata_pointer(self._hdata, self._ptr, "own_lines") - return W.hdata_integer(W.hdata_get("lines"), own_lines, "lines_count") - - @property - def lines(self): - own_lines = W.hdata_pointer(self._hdata, self._ptr, "own_lines") - - if own_lines: - hdata_line = W.hdata_get("line") - - line_pointer = W.hdata_pointer( - W.hdata_get("lines"), own_lines, "last_line" - ) - - while line_pointer: - data_pointer = W.hdata_pointer( - hdata_line, line_pointer, "data" - ) - - if data_pointer: - yield WeechatChannelBuffer.Line(data_pointer) - - line_pointer = W.hdata_move(hdata_line, line_pointer, -1) - - def _print(self, string): - # type: (str) -> None - """ Print a string to the room buffer """ - W.prnt(self._ptr, string) - - def print_date_tags(self, data, date=None, tags=None): - # type: (str, Optional[int], Optional[List[str]]) -> None - date = date or int(time.time()) - tags = tags or [] - - tags_string = ",".join(tags) - W.prnt_date_tags(self._ptr, date, tags_string, data) - - def error(self, string): - # type: (str) -> None - """ Print an error to the room buffer """ - message = "{prefix}{script}: {message}".format( - prefix=W.prefix("error"), script=SCRIPT_NAME, message=string - ) - - self._print(message) - - def info(self, string): - message = "{prefix}{script}: {message}".format( - prefix=W.prefix("network"), script=SCRIPT_NAME, message=string - ) - self._print(message) - - @staticmethod - def _color_for_tags(color): - # type: (str) -> str - if color == "weechat.color.chat_nick_self": - option = W.config_get(color) - return W.config_string(option) - - return color - - def _message_tags(self, user, message_type): - # type: (WeechatUser, str) -> List[str] - tags = list(self.tags[message_type]) - - tags.append("nick_{nick}".format(nick=user.nick)) - - color = self._color_for_tags(user.color) - - if message_type not in ("action", "notice"): - tags.append("prefix_nick_{color}".format(color=color)) - - return tags - - def _get_user(self, nick): - # type: (str) -> WeechatUser - if nick in self.users: - return self.users[nick] - - # A message from a non joined user - return WeechatUser(nick) - - def _print_message(self, user, message, date, tags, extra_prefix=""): - prefix_string = ( - extra_prefix - if not user.prefix - else "{}{}{}{}".format( - extra_prefix, - W.color(self._get_prefix_color(user.prefix)), - user.prefix, - W.color("reset"), - ) - ) - - data = "{prefix}{color}{author}{ncolor}\t{msg}".format( - prefix=prefix_string, - color=W.color(user.color), - author=user.nick, - ncolor=W.color("reset"), - msg=message, - ) - - self.print_date_tags(data, date, tags) - - def message(self, nick, message, date, extra_tags=None, extra_prefix=""): - # type: (str, str, int, List[str], str) -> None - user = self._get_user(nick) - tags_type = "message_private" if self.type == "private" else "message" - tags = self._message_tags(user, tags_type) + (extra_tags or []) - self._print_message(user, message, date, tags, extra_prefix) - - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - def notice(self, nick, message, date, extra_tags=None, extra_prefix=""): - # type: (str, str, int, Optional[List[str]], str) -> None - user = self._get_user(nick) - user_prefix = ( - "" - if not user.prefix - else "{}{}{}".format( - W.color(self._get_prefix_color(user.prefix)), - user.prefix, - W.color("reset"), - ) - ) - - user_string = "{}{}{}{}".format( - user_prefix, W.color(user.color), user.nick, W.color("reset") - ) - - data = ( - "{extra_prefix}{prefix}{color}Notice" - "{del_color}({ncolor}{user}{del_color}){ncolor}" - ": {message}" - ).format( - extra_prefix=extra_prefix, - prefix=W.prefix("network"), - color=W.color("irc.color.notice"), - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - user=user_string, - message=message, - ) - - tags = self._message_tags(user, "notice") + (extra_tags or []) - self.print_date_tags(data, date, tags) - - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - def _format_action(self, user, message): - nick_prefix = ( - "" - if not user.prefix - else "{}{}{}".format( - W.color(self._get_prefix_color(user.prefix)), - user.prefix, - W.color("reset"), - ) - ) - - data = ( - "{nick_prefix}{nick_color}{author}" - "{ncolor} {msg}").format( - nick_prefix=nick_prefix, - nick_color=W.color(user.color), - author=user.nick, - ncolor=W.color("reset"), - msg=message, - ) - return data - - def _print_action(self, user, message, date, tags, extra_prefix=""): - data = self._format_action(user, message) - data = "{extra_prefix}{prefix}{data}".format( - extra_prefix=extra_prefix, - prefix=W.prefix("action"), - data=data) - - self.print_date_tags(data, date, tags) - - def action(self, nick, message, date, extra_tags=None, extra_prefix=""): - # type: (str, str, int, Optional[List[str]], str) -> None - user = self._get_user(nick) - tags_type = "action_private" if self.type == "private" else "action" - tags = self._message_tags(user, tags_type) + (extra_tags or []) - self._print_action(user, message, date, tags, extra_prefix) - - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - @staticmethod - def _get_nicklist_group(user): - # type: (WeechatUser) -> str - group_name = "999|..." - - if user.prefix == "&": - group_name = "000|o" - elif user.prefix == "@": - group_name = "001|h" - elif user.prefix == "+": - group_name = "002|v" - - return group_name - - @staticmethod - def _get_prefix_color(prefix): - # type: (str) -> str - - return G.CONFIG.color.nick_prefixes.get(prefix, "") - - def _add_user_to_nicklist(self, user): - # type: (WeechatUser) -> None - nick_pointer = W.nicklist_search_nick(self._ptr, "", user.nick) - - if not nick_pointer: - group = W.nicklist_search_group( - self._ptr, "", self._get_nicklist_group(user) - ) - prefix = user.prefix if user.prefix else " " - W.nicklist_add_nick( - self._ptr, - group, - user.nick, - user.color, - prefix, - self._get_prefix_color(user.prefix), - 1, - ) - - def _membership_message(self, user, message_type): - # type: (WeechatUser, str) -> str - action_color = "green" if message_type in ("join", "invite") else "red" - prefix = "join" if message_type in ("join", "invite") else "quit" - - membership_message = self.membership_messages[message_type] - - message = ( - "{prefix}{color}{author}{ncolor} " - "{del_color}({host_color}{host}{del_color})" - "{action_color} {message} " - "{channel_color}{room}{ncolor}" - ).format( - prefix=W.prefix(prefix), - color=W.color(user.color), - author=user.nick, - ncolor=W.color("reset"), - del_color=W.color("chat_delimiters"), - host_color=W.color("chat_host"), - host=user.host, - action_color=W.color(action_color), - message=membership_message, - channel_color=W.color("chat_channel"), - room=self.short_name, - ) - - return message - - def join(self, user, date, message=True, extra_tags=None): - # type: (WeechatUser, int, Optional[bool], Optional[List[str]]) -> None - self._add_user_to_nicklist(user) - self.users[user.nick] = user - - if len(self.users) > 2: - W.buffer_set(self._ptr, "localvar_set_type", "channel") - - if message: - tags = self._message_tags(user, "join") - msg = self._membership_message(user, "join") - - # TODO add a option to disable smart filters - tags.append(SCRIPT_NAME + "_smart_filter") - - self.print_date_tags(msg, date, tags) - self.add_smart_filtered_nick(user.nick) - - def invite(self, nick, date, extra_tags=None): - # type: (str, int, Optional[List[str]]) -> None - user = self._get_user(nick) - tags = self._message_tags(user, "invite") - message = self._membership_message(user, "invite") - self.print_date_tags(message, date, tags + (extra_tags or [])) - - def remove_user_from_nicklist(self, user): - # type: (WeechatUser) -> None - nick_pointer = W.nicklist_search_nick(self._ptr, "", user.nick) - - if nick_pointer: - W.nicklist_remove_nick(self._ptr, nick_pointer) - - def _leave(self, nick, date, message, leave_type, extra_tags=None): - # type: (str, int, bool, str, List[str]) -> None - user = self._get_user(nick) - self.remove_user_from_nicklist(user) - - if len(self.users) <= 2: - W.buffer_set(self._ptr, "localvar_set_type", "private") - - if message: - tags = self._message_tags(user, leave_type) - - # TODO make this configurable - if not user.spoken_recently: - tags.append(SCRIPT_NAME + "_smart_filter") - - msg = self._membership_message(user, leave_type) - self.print_date_tags(msg, date, tags + (extra_tags or [])) - self.remove_smart_filtered_nick(user.nick) - - if user.nick in self.users: - del self.users[user.nick] - - def part(self, nick, date, message=True, extra_tags=None): - # type: (str, int, bool, Optional[List[str]]) -> None - self._leave(nick, date, message, "part", extra_tags) - - def kick(self, nick, date, message=True, extra_tags=None): - # type: (str, int, bool, Optional[List[str]]) -> None - self._leave(nick, date, message, "kick", extra_tags) - - def _print_topic(self, nick, topic, date): - user = self._get_user(nick) - tags = self._message_tags(user, "topic") - - data = ( - "{prefix}{nick} has changed " - "the topic for {chan_color}{room}{ncolor} " - 'to "{topic}"' - ).format( - prefix=W.prefix("network"), - nick=user.nick, - chan_color=W.color("chat_channel"), - ncolor=W.color("reset"), - room=self.short_name, - topic=topic, - ) - - self.print_date_tags(data, date, tags) - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - @property - def topic(self): - return W.buffer_get_string(self._ptr, "title") - - @topic.setter - def topic(self, topic): - W.buffer_set(self._ptr, "title", topic) - - def change_topic(self, nick, topic, date, message=True): - if message: - self._print_topic(nick, topic, date) - - self.topic = topic - self.topic_author = nick - self.topic_date = date - - def self_message(self, nick, message, date, tags=None): - user = self._get_user(nick) - tags = self._message_tags(user, "self_message") + (tags or []) - self._print_message(user, message, date, tags) - - def self_action(self, nick, message, date, tags=None): - user = self._get_user(nick) - tags = self._message_tags(user, "self_message") + (tags or []) - tags.append(SCRIPT_NAME + "_action") - self._print_action(user, message, date, tags) - - @property - def type(self): - return W.buffer_get_string(self._ptr, "localvar_type") - - @property - def short_name(self): - return W.buffer_get_string(self._ptr, "short_name") - - @short_name.setter - def short_name(self, name): - W.buffer_set(self._ptr, "short_name", name) - - @property - def name(self): - return W.buffer_get_string(self._ptr, "name") - - @name.setter - def name(self, name): - W.buffer_set(self._ptr, "name", name) - - @property - def number(self): - """Get the buffer number, starts at 1.""" - return int(W.buffer_get_integer(self._ptr, "number")) - - @number.setter - def number(self, n): - W.buffer_set(self._ptr, "number", str(n)) - - def find_lines(self, predicate, max_lines=None): - lines = [] - count = 0 - for line in self.lines: - if predicate(line): - lines.append(line) - count += 1 - if max_lines is not None and count == max_lines: - return lines - - return lines - - -class RoomBuffer(object): - def __init__(self, room, server_name, homeserver, prev_batch): - self.room = room - self.homeserver = homeserver - self._backlog_pending = False - self.prev_batch = prev_batch - self.joined = True - self.leave_event_id = None # type: Optional[str] - self.members_fetched = False - self.first_view = True - self.first_backlog_request = True - self.unhandled_users = [] # type: List[str] - self.inactive_users = [] - - self.sent_messages_queue = dict() # type: Dict[UUID, OwnMessage] - self.printed_before_ack_queue = list() # type: List[UUID] - self.undecrypted_events = deque(maxlen=5000) - - self.typing_notice_time = None - self._typing = False - self.typing_enabled = True - - self.last_read_event = None - self._read_markers_enabled = True - self.server_name = server_name - - self.last_message = None - - buffer_name = "{}{}.{}".format(G.BUFFER_NAME_PREFIX, server_name, room.room_id) - - # This dict remembers the connection from a user_id to the name we - # displayed in the buffer - self.displayed_nicks = {} - user = shorten_sender(self.room.own_user_id) - - self.weechat_buffer = WeechatChannelBuffer( - buffer_name, server_name, user - ) - - W.buffer_set( - self.weechat_buffer._ptr, - "localvar_set_domain", - self.homeserver.hostname - ) - - W.buffer_set( - self.weechat_buffer._ptr, - "localvar_set_room_id", - room.room_id - ) - - if room.canonical_alias: - self.update_canonical_alias_localvar() - - @property - def backlog_pending(self): - return self._backlog_pending - - @backlog_pending.setter - def backlog_pending(self, value): - self._backlog_pending = value - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - @property - def warning_prefix(self): - return G.CONFIG.look.encryption_warning_sign - - @property - def typing(self): - # type: () -> bool - """Return our typing status.""" - return self._typing - - @typing.setter - def typing(self, value): - self._typing = value - if value: - self.typing_notice_time = time.time() - else: - self.typing_notice_time = None - - @property - def typing_notice_expired(self): - # type: () -> bool - """Check if the typing notice has expired. - - Returns true if a new typing notice should be sent. - """ - if not self.typing_notice_time: - return True - - now = time.time() - if (now - self.typing_notice_time) > (TYPING_NOTICE_TIMEOUT / 1000): - return True - return False - - @property - def should_send_read_marker(self): - # type () -> bool - """Check if we need to send out a read receipt.""" - if not self.read_markers_enabled: - return False - - if not self.last_read_event: - return True - - if self.last_read_event == self.last_event_id: - return False - - return True - - @property - def last_event_id(self): - # type () -> str - """Get the event id of the last shown matrix event.""" - for line in self.weechat_buffer.lines: - for tag in line.tags: - if tag.startswith("matrix_id"): - event_id = tag[10:] - return event_id - - return "" - - @property - def printed_event_ids(self): - for line in self.weechat_buffer.lines: - for tag in line.tags: - if tag.startswith("matrix_id"): - event_id = tag[10:] - yield event_id - - @property - def read_markers_enabled(self): - # type: () -> bool - """Check if read receipts are enabled for this room.""" - return bool(int(W.string_eval_expression( - G.CONFIG.network.read_markers_conditions, - {}, - {"markers_enabled": str(int(self._read_markers_enabled))}, - {"type": "condition"} - ))) - - @read_markers_enabled.setter - def read_markers_enabled(self, value): - self._read_markers_enabled = value - - def find_nick(self, user_id): - # type: (str) -> str - """Find a suitable nick from a user_id.""" - if user_id in self.displayed_nicks: - return self.displayed_nicks[user_id] - - return user_id - - def add_user(self, user_id, date, is_state, force_add=False): - # User is already added don't add him again. - if user_id in self.displayed_nicks: - return - - try: - user = self.room.users[user_id] - except KeyError: - # No user found, he must have left already in an event that is - # yet to come, so do nothing - return - - # Adding users to the nicklist is a O(1) + search time - # operation (the nicks are added to a linked list sorted). - # The search time is O(N * min(a,b)) where N is the number - # of nicks already added and a/b are the length of - # the strings that are compared at every iteration. - # Because the search time get's increasingly longer we're - # going to stop adding inactive users, they will be lazily added if - # they become active. - if is_state and not force_add and user.power_level <= 0: - if (len(self.displayed_nicks) >= - G.CONFIG.network.max_nicklist_users): - self.inactive_users.append(user_id) - return - - try: - self.inactive_users.remove(user_id) - except ValueError: - pass - - short_name = shorten_sender(user.user_id) - - # TODO handle this special case for discord bridge users and - # freenode bridge users better - if (user.user_id.startswith("@_discord_") or - user.user_id.startswith("@_slack_") or - user.user_id.startswith("@_discordpuppet_") or - user.user_id.startswith("@_slackpuppet_") or - user.user_id.startswith("@whatsapp_") or - user.user_id.startswith("@facebook_") or - user.user_id.startswith("@telegram_") or - user.user_id.startswith("@_telegram_") or - user.user_id.startswith("@_xmpp_") or - user.user_id.startswith("@irc_")): - if user.display_name: - short_name = user.display_name[0:50] - elif user.user_id.startswith("@twilio_"): - short_name = shorten_sender(user.user_id[7:]) - elif user.user_id.startswith("@freenode_"): - short_name = shorten_sender(user.user_id[9:]) - elif user.user_id.startswith("@_ircnet_"): - short_name = shorten_sender(user.user_id[8:]) - elif user.user_id.startswith("@gitter_"): - short_name = shorten_sender(user.user_id[7:]) - - # TODO make this configurable - if not short_name or short_name in self.displayed_nicks.values(): - # Use the full user id, but don't include the @ - nick = user_id[1:] - else: - nick = short_name - - buffer_user = RoomUser(nick, user_id, user.power_level, date) - self.displayed_nicks[user_id] = nick - - if self.room.own_user_id == user_id: - buffer_user.color = "weechat.color.chat_nick_self" - user.nick_color = "weechat.color.chat_nick_self" - - self.weechat_buffer.join(buffer_user, date, not is_state) - - def handle_membership_events(self, event, is_state): - date = server_ts_to_weechat(event.server_timestamp) - - if event.content["membership"] == "join": - if (event.state_key not in self.displayed_nicks - and event.state_key not in self.inactive_users): - if len(self.room.users) > 100: - self.unhandled_users.append(event.state_key) - return - - self.add_user(event.state_key, date, is_state) - else: - # TODO print out profile changes - return - - elif event.content["membership"] == "leave": - if event.state_key in self.unhandled_users: - self.unhandled_users.remove(event.state_key) - return - - nick = self.find_nick(event.state_key) - if event.sender == event.state_key: - self.weechat_buffer.part(nick, date, not is_state) - else: - self.weechat_buffer.kick(nick, date, not is_state) - - if event.state_key in self.displayed_nicks: - del self.displayed_nicks[event.state_key] - - # We left the room, remember the event id of our leave, if we - # rejoin we get events that came before this event as well as - # after our leave, this way we know where to continue - if event.state_key == self.room.own_user_id: - self.leave_event_id = event.event_id - - elif event.content["membership"] == "invite": - if is_state: - return - - self.weechat_buffer.invite(event.state_key, date) - return - - self.update_buffer_name() - - def update_buffer_name(self): - if self.room.is_named: - if self.room.name and self.room.name != "#": - room_name = self.room.name - room_name = (room_name if room_name.startswith("#") - else "#" + room_name) - elif self.room.canonical_alias: - room_name = self.room.canonical_alias - self.update_canonical_alias_localvar() - elif self.room.name == "#": - room_name = "##" - else: - room_name = self.room.display_name - - if room_name is None: - # Use placeholder room name - room_name = 'Empty room (?)' - - self.weechat_buffer.short_name = room_name - - if G.CONFIG.human_buffer_names: - buffer_name = "{}.{}".format(self.server_name, room_name) - self.weechat_buffer.name = buffer_name - - def update_canonical_alias_localvar(self): - W.buffer_set( - self.weechat_buffer._ptr, - "localvar_set_canonical_alias", - self.room.canonical_alias - ) - - def _redact_line(self, event): - def predicate(event_id, line): - def already_redacted(tags): - if SCRIPT_NAME + "_redacted" in tags: - return True - return False - - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - tags = line.tags - - if event_tag in tags and not already_redacted(tags): - return True - - return False - - def redact_string(message): - new_message = "" - - if G.CONFIG.look.redactions == RedactType.STRIKETHROUGH: - plaintext_msg = W.string_remove_color(message, "") - new_message = string_strikethrough(plaintext_msg) - elif G.CONFIG.look.redactions == RedactType.NOTICE: - new_message = message - elif G.CONFIG.look.redactions == RedactType.DELETE: - pass - - return new_message - - lines = self.weechat_buffer.find_lines( - partial(predicate, event.redacts) - ) - - # No line to redact, return early - if not lines: - return - - censor = self.find_nick(event.sender) - redaction_msg = Render.redacted(censor, event.reason) - - line = lines[0] - message = line.message - tags = line.tags - - new_message = redact_string(message) - message = " ".join(s for s in [new_message, redaction_msg] if s) - tags.append("matrix_redacted") - - line.message = message - line.tags = tags - - for line in lines[1:]: - message = line.message - tags = line.tags - - new_message = redact_string(message) - - if not new_message: - new_message = redaction_msg - elif G.CONFIG.look.redactions == RedactType.NOTICE: - new_message += " {}".format(redaction_msg) - - tags.append("matrix_redacted") - - line.message = new_message - line.tags = tags - - def _handle_topic(self, event, is_state): - nick = self.find_nick(event.sender) - - self.weechat_buffer.change_topic( - nick, - event.topic, - server_ts_to_weechat(event.server_timestamp), - not is_state, - ) - - @staticmethod - def get_event_tags(event): - # type: (Event) -> List[str] - tags = [SCRIPT_NAME + "_id_{}".format(event.event_id)] - if event.sender_key: - tags.append(SCRIPT_NAME + "_senderkey_{}".format(event.sender_key)) - if event.session_id: - tags.append(SCRIPT_NAME + "_session_id_{}".format( - event.session_id - )) - - return tags - - def _handle_power_level(self, _): - for user_id in self.room.power_levels.users: - if user_id in self.displayed_nicks: - nick = self.find_nick(user_id) - - user = self.weechat_buffer.users[nick] - user.power_level = self.room.power_levels.get_user_level( - user_id - ) - - # There is no way to change the group of a user without - # removing him from the nicklist - self.weechat_buffer.remove_user_from_nicklist(user) - self.weechat_buffer._add_user_to_nicklist(user) - - def handle_state_event(self, event): - if isinstance(event, RoomMemberEvent): - self.handle_membership_events(event, True) - elif isinstance(event, RoomTopicEvent): - self._handle_topic(event, True) - elif isinstance(event, PowerLevelsEvent): - self._handle_power_level(event) - elif isinstance(event, (RoomNameEvent, RoomAliasEvent)): - self.update_buffer_name() - elif isinstance(event, RoomEncryptionEvent): - pass - - def handle_own_message_in_timeline(self, event): - """Check if our own message is already printed if not print it. - This function is called for messages that contain a transaction id - indicating that they were sent out using our own client. If we sent out - a message but never got a valid server response (e.g. due to - disconnects) this function prints them out using data from the next - sync response""" - uuid = UUID(event.transaction_id) - message = self.sent_messages_queue.pop(uuid, None) - - # We already got a response to the room_send_message() API call and - # handled the message, no need to print it out again - if not message: - return - - message.event_id = event.event_id - if uuid in self.printed_before_ack_queue: - self.replace_printed_line_by_uuid( - event.transaction_id, - message - ) - self.printed_before_ack_queue.remove(uuid) - return - - if isinstance(message, OwnAction): - self.self_action(message) - elif isinstance(message, OwnMessage): - self.self_message(message) - return - - def print_room_message(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - - data = Render.message(event.body, event.formatted_body) - - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - date = server_ts_to_weechat(event.server_timestamp) - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_room_emote(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.action( - nick, event.body, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_room_notice(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.notice( - nick, event.body, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_room_media(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - if isinstance(event, RoomMessageMedia): - data = Render.media(event.url, event.body, self.homeserver.geturl()) - else: - data = Render.encrypted_media( - event.url, event.body, event.key["k"], event.hashes["sha256"], - event.iv, self.homeserver.geturl() - ) - - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_unknown(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - data = Render.unknown(event.type, event.content) - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_redacted(self, event, extra_tags=None): - extra_tags = extra_tags or [] - - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - tags = self.get_event_tags(event) - tags.append(SCRIPT_NAME + "_redacted") - tags += extra_tags - - censor = self.find_nick(event.redacter) - data = Render.redacted(censor, event.reason) - - self.weechat_buffer.message(nick, data, date, tags) - - def print_room_encryption(self, event, extra_tags=None): - nick = self.find_nick(event.sender) - data = Render.room_encryption(nick) - # TODO this should also have tags - self.weechat_buffer.info(data) - - def print_megolm(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - - data = Render.megolm() - - session_id_tag = SCRIPT_NAME + "_sessionid_" + event.session_id - self.weechat_buffer.message( - nick, - data, - date, - self.get_event_tags(event) + [session_id_tag] + extra_tags - ) - - self.undecrypted_events.append(event) - - def print_bad_event(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - data = Render.bad(event) - extra_prefix = self.warning_prefix - - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def handle_room_messages(self, event, extra_tags=None): - if isinstance(event, RoomMessageEmote): - self.print_room_emote(event, extra_tags) - - elif isinstance(event, RoomMessageText): - self.print_room_message(event, extra_tags) - - elif isinstance(event, RoomMessageNotice): - self.print_room_notice(event, extra_tags) - - elif isinstance(event, RoomMessageMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomEncryptedMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomMessageUnknown): - self.print_unknown(event, extra_tags) - - elif isinstance(event, RoomEncryptionEvent): - self.print_room_encryption(event, extra_tags) - - elif isinstance(event, MegolmEvent): - self.print_megolm(event, extra_tags) - - def force_load_member(self, event): - if (event.sender not in self.displayed_nicks and - event.sender in self.room.users): - - try: - self.unhandled_users.remove(event.sender) - except ValueError: - pass - - self.add_user(event.sender, 0, True, True) - - def handle_timeline_event(self, event, extra_tags=None): - # TODO this should be done for every messagetype that gets printed in - # the buffer - if isinstance(event, (RoomMessage, MegolmEvent)): - self.force_load_member(event) - - if event.transaction_id: - self.handle_own_message_in_timeline(event) - return - - if isinstance(event, RoomMemberEvent): - self.handle_membership_events(event, False) - - elif isinstance(event, (RoomNameEvent, RoomAliasEvent)): - self.update_buffer_name() - - elif isinstance(event, RoomTopicEvent): - self._handle_topic(event, False) - - # Emotes are a subclass of RoomMessageText, so put them before the text - # ones - elif isinstance(event, RoomMessageEmote): - self.print_room_emote(event, extra_tags) - - elif isinstance(event, RoomMessageText): - self.print_room_message(event, extra_tags) - - elif isinstance(event, RoomMessageNotice): - self.print_room_notice(event, extra_tags) - - elif isinstance(event, RoomMessageMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomEncryptedMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomMessageUnknown): - self.print_unknown(event, extra_tags) - - elif isinstance(event, RedactionEvent): - self._redact_line(event) - - elif isinstance(event, RedactedEvent): - self.print_redacted(event, extra_tags) - - elif isinstance(event, RoomEncryptionEvent): - self.print_room_encryption(event, extra_tags) - - elif isinstance(event, PowerLevelsEvent): - # TODO we should print out a message for this event - self._handle_power_level(event) - - elif isinstance(event, MegolmEvent): - self.print_megolm(event, extra_tags) - - elif isinstance(event, UnknownEvent): - pass - - elif isinstance(event, BadEvent): - self.print_bad_event(event, extra_tags) - - elif isinstance(event, UnknownBadEvent): - self.error("Unknown bad event: {}".format( - pprint.pformat(event.source) - )) - - else: - W.prnt( - "", "Unhandled event of type {}.".format(type(event).__name__) - ) - - def self_message(self, message): - # type: (OwnMessage) -> None - nick = self.find_nick(self.room.own_user_id) - data = message.formatted_message.to_weechat() - if message.event_id: - tags = [SCRIPT_NAME + "_id_{}".format(message.event_id)] - else: - tags = [SCRIPT_NAME + "_uuid_{}".format(message.uuid)] - date = message.age - - self.weechat_buffer.self_message(nick, data, date, tags) - - def self_action(self, message): - # type: (OwnMessage) -> None - nick = self.find_nick(self.room.own_user_id) - date = message.age - if message.event_id: - tags = [SCRIPT_NAME + "_id_{}".format(message.event_id)] - else: - tags = [SCRIPT_NAME + "_uuid_{}".format(message.uuid)] - - self.weechat_buffer.self_action( - nick, message.formatted_message.to_weechat(), date, tags - ) - - @staticmethod - def _find_by_uuid_predicate(uuid, line): - uuid_tag = SCRIPT_NAME + "_uuid_{}".format(uuid) - tags = line.tags - - if uuid_tag in tags: - return True - return False - - def mark_message_as_unsent(self, uuid, _): - """Append to already printed lines that are greyed out an error - message""" - lines = self.weechat_buffer.find_lines( - partial(self._find_by_uuid_predicate, uuid) - ) - last_line = lines[-1] - - message = last_line.message - message += (" {del_color}<{ncolor}{error_color}Error sending " - "message{del_color}>{ncolor}").format( - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - error_color=W.color(color_pair( - G.CONFIG.color.error_message_fg, - G.CONFIG.color.error_message_bg))) - - last_line.message = message - - def replace_printed_line_by_uuid(self, uuid, new_message): - """Replace already printed lines that are greyed out with real ones.""" - if isinstance(new_message, OwnAction): - displayed_nick = self.displayed_nicks[self.room.own_user_id] - user = self.weechat_buffer._get_user(displayed_nick) - data = self.weechat_buffer._format_action( - user, - new_message.formatted_message.to_weechat() - ) - new_lines = data.split("\n") - else: - new_lines = new_message.formatted_message.to_weechat().split("\n") - - line_count = len(new_lines) - - lines = self.weechat_buffer.find_lines( - partial(self._find_by_uuid_predicate, uuid), line_count - ) - - for i, line in enumerate(lines): - line.message = new_lines[i] - tags = line.tags - - new_tags = [ - tag for tag in tags - if not tag.startswith(SCRIPT_NAME + "_uuid_") - ] - new_tags.append(SCRIPT_NAME + "_id_" + new_message.event_id) - line.tags = new_tags - - def replace_undecrypted_line(self, event): - """Find an undecrypted message in the buffer and replace it with the now - decrypted event.""" - # TODO different messages need different formatting - # To implement this, refactor out the different formatting code - # snippets to a Formatter class and reuse them here. - if not isinstance(event, RoomMessageText): - return - - def predicate(event_id, line): - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - if event_tag in line.tags: - return True - return False - - lines = self.weechat_buffer.find_lines( - partial(predicate, event.event_id) - ) - - if not lines: - return - - formatted = None - if event.formatted_body: - formatted = Formatted.from_html(event.formatted_body) - - data = formatted.to_weechat() if formatted else event.body - # TODO this isn't right if the data has multiple lines, that is - # everything is printed on a single line and newlines are shown as a - # space. - # Weechat should support deleting lines and printing new ones at an - # arbitrary position. - # To implement this without weechat support either only handle single - # line messages or edit the first line in place, print new ones at the - # bottom and sort the buffer lines. - lines[0].message = data - - def old_message(self, event): - tags = list(self.weechat_buffer.tags["old_message"]) - # TODO events that change the room state (topics, membership changes, - # etc...) should be printed out as well, but for this to work without - # messing up the room state the state change will need to be separated - # from the print logic. - if isinstance(event, RoomMessage): - self.force_load_member(event) - self.handle_room_messages(event, tags) - - elif isinstance(event, MegolmEvent): - self.print_megolm(event, tags) - - elif isinstance(event, RedactedEvent): - self.print_redacted(event, tags) - - elif isinstance(event, BadEvent): - self.print_bad_event(event, tags) - - def sort_messages(self): - class LineCopy(object): - def __init__( - self, date, date_printed, tags, prefix, message, highlight - ): - self.date = date - self.date_printed = date_printed - self.tags = tags - self.prefix = prefix - self.message = message - self.highlight = highlight - - @classmethod - def from_line(cls, line): - return cls( - line.date, - line.date_printed, - line.tags, - line.prefix, - line.message, - line.highlight, - ) - - lines = [ - LineCopy.from_line(line) for line in self.weechat_buffer.lines - ] - sorted_lines = sorted(lines, key=lambda line: line.date, reverse=True) - - for line_number, line in enumerate(self.weechat_buffer.lines): - new = sorted_lines[line_number] - line.update( - new.date, new.date_printed, new.tags, new.prefix, new.message - ) - - def handle_backlog(self, response): - self.prev_batch = response.end - - for event in response.chunk: - # The first backlog request seems to have a race condition going on - # where we receive a message in a sync response, get a prev_batch, - # yet when we request older messages with the prev_batch the same - # message might appear in the room messages response. This only - # seems to happen if the message is relatively recently sent. - # Because of this we check if our first backlog request contains - # some already printed events, if so; skip printing them. - if (self.first_backlog_request - and event.event_id in self.printed_event_ids): - continue - - self.old_message(event) - - self.sort_messages() - - self.first_backlog_request = False - self.backlog_pending = False - - def handle_joined_room(self, info): - for event in info.state: - self.handle_state_event(event) - - timeline_events = None - - # This is a rejoin, skip already handled events - if not self.joined: - leave_index = None - - for i, event in enumerate(info.timeline.events): - if event.event_id == self.leave_event_id: - leave_index = i - break - - if leave_index: - timeline_events = info.timeline.events[leave_index + 1:] - # Handle our leave as a state event since we're not in the - # nicklist anymore but we're already printed out our leave - self.handle_state_event(info.timeline.events[leave_index]) - else: - timeline_events = info.timeline.events - - # mark that we are now joined - self.joined = True - - else: - timeline_events = info.timeline.events - - for event in timeline_events: - self.handle_timeline_event(event) - - for event in info.account_data: - if isinstance(event, FullyReadEvent): - if event.event_id == self.last_event_id: - current_buffer = W.buffer_search("", "") - - if self.weechat_buffer._ptr == current_buffer: - continue - - W.buffer_set(self.weechat_buffer._ptr, "unread", "") - W.buffer_set(self.weechat_buffer._ptr, "hotlist", "-1") - - # We didn't handle all joined users, the room display name might still - # be outdated because of that, update it now. - if self.unhandled_users: - self.update_buffer_name() - - def handle_left_room(self, info): - self.joined = False - - for event in info.state: - self.handle_state_event(event) - - for event in info.timeline.events: - self.handle_timeline_event(event) - - def error(self, string): - # type: (str) -> None - self.weechat_buffer.error(string) diff --git a/weechat/python/matrix/colors.py b/weechat/python/matrix/colors.py deleted file mode 100644 index c00bc0d..0000000 --- a/weechat/python/matrix/colors.py +++ /dev/null @@ -1,1285 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2008 Nicholas Marriott -# Copyright © 2016 Avi Halachmi -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import html -import re -import textwrap - -# pylint: disable=redefined-builtin -from builtins import str -from collections import namedtuple -from typing import Dict, List, Optional, Union - -import webcolors -from pygments import highlight -from pygments.formatter import Formatter, get_style_by_name -from pygments.lexers import get_lexer_by_name -from pygments.util import ClassNotFound - -from . import globals as G -from .globals import W -from .utils import (string_strikethrough, - string_color_and_reset, - color_pair, - text_block, - colored_text_block) - -try: - from HTMLParser import HTMLParser -except ImportError: - from html.parser import HTMLParser - - -class FormattedString: - __slots__ = ("text", "attributes") - - def __init__(self, text, attributes): - self.attributes = DEFAULT_ATTRIBUTES.copy() - self.attributes.update(attributes) - self.text = text - - -class Formatted(object): - def __init__(self, substrings): - # type: (List[FormattedString]) -> None - self.substrings = substrings - - def textwrapper(self, width, colors): - return textwrap.TextWrapper( - width=width, - initial_indent="{}> ".format(W.color(colors)), - subsequent_indent="{}> ".format(W.color(colors)), - ) - - def is_formatted(self): - # type: (Formatted) -> bool - for string in self.substrings: - if string.attributes != DEFAULT_ATTRIBUTES: - return True - return False - - # TODO reverse video - @classmethod - def from_input_line(cls, line): - # type: (str) -> Formatted - """Parses the weechat input line and produces formatted strings that - can be later converted to HTML or to a string for weechat's print - functions - """ - text = "" # type: str - substrings = [] # type: List[FormattedString] - attributes = DEFAULT_ATTRIBUTES.copy() - - # If this is false, only IRC formatting characters will be parsed. - do_markdown = G.CONFIG.look.markdown_input - - # Disallow backticks in URLs so that code blocks are unaffected by the - # URL handling - url_regex = r"\b[a-z]+://[^\s`]+" - - # Escaped things are not markdown delimiters, so substitute them away - # when (quickly) looking for the last delimiters in the line. - # Additionally, URLs are ignored for the purposes of markdown - # delimiters. - # Note that the replacement needs to be the same length as the original - # for the indices to be correct. - escaped_masked = re.sub( - r"\\[\\*_`]|(?:" + url_regex + ")", - lambda m: "a" * len(m.group(0)), - line - ) - - def last_match_index(regex, offset_in_match): - matches = list(re.finditer(regex, escaped_masked)) - return matches[-1].span()[0] + offset_in_match if matches else -1 - - # 'needs_word': whether the wrapper must surround words, for example - # '*italic*' and not '* not-italic *'. - # 'validate': whether it can occur within the current attributes - wrappers = { - "**": { - "key": "bold", - "last_index": last_match_index(r"\S\*\*", 1), - "needs_word": True, - "validate": lambda attrs: not attrs["code"], - }, - "*": { - "key": "italic", - "last_index": last_match_index(r"\S\*($|[^*])", 1), - "needs_word": True, - "validate": lambda attrs: not attrs["code"], - }, - "_": { - "key": "italic", - "last_index": last_match_index(r"\S_", 1), - "needs_word": True, - "validate": lambda attrs: not attrs["code"], - }, - "`": { - "key": "code", - "last_index": last_match_index(r"`", 0), - "needs_word": False, - "validate": lambda attrs: True, - } - } - wrapper_init_chars = set(k[0] for k in wrappers.keys()) - wrapper_max_len = max(len(k) for k in wrappers.keys()) - - irc_toggles = { - "\x02": "bold", - "\x1D": "italic", - "\x1F": "underline", - } - - # Characters that consume a prefixed backslash - escapable_chars = wrapper_init_chars.copy() - escapable_chars.add("\\") - - # Collect URL spans - url_spans = [m.span() for m in re.finditer(url_regex, line)] - url_spans.reverse() # we'll be popping from the end - - # Whether we are currently in a URL - in_url = False - - i = 0 - while i < len(line): - # Update the 'in_url' flag. The first condition is not a while loop - # because URLs must contain '://', ensuring that we will not skip 2 - # URLs in one iteration. - if url_spans and i >= url_spans[-1][1]: - in_url = False - url_spans.pop() - if url_spans and i >= url_spans[-1][0]: - in_url = True - - # Markdown escape - if do_markdown and \ - i + 1 < len(line) and line[i] == "\\" \ - and (line[i + 1] in escapable_chars - if not attributes["code"] - else line[i + 1] == "`") \ - and not in_url: - text += line[i + 1] - i = i + 2 - - # IRC bold/italic/underline - elif line[i] in irc_toggles and not attributes["code"]: - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - key = irc_toggles[line[i]] - attributes[key] = not attributes[key] - i = i + 1 - - # IRC reset - elif line[i] == "\x0F" and not attributes["code"]: - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - # Reset all the attributes - attributes = DEFAULT_ATTRIBUTES.copy() - i = i + 1 - - # IRC color - elif line[i] == "\x03" and not attributes["code"]: - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - i = i + 1 - - # check if it's a valid color, add it to the attributes - if line[i].isdigit(): - color_string = line[i] - i = i + 1 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["fgcolor"] = color_line_to_weechat(color_string) - else: - attributes["fgcolor"] = None - - # check if we have a background color - if line[i] == "," and line[i + 1].isdigit(): - color_string = line[i + 1] - i = i + 2 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["bgcolor"] = color_line_to_weechat(color_string) - else: - attributes["bgcolor"] = None - - # Markdown wrapper (emphasis/bold/code) - elif do_markdown and line[i] in wrapper_init_chars and not in_url: - for l in range(wrapper_max_len, 0, -1): - if i + l <= len(line) and line[i : i + l] in wrappers: - descriptor = wrappers[line[i : i + l]] - - if not descriptor["validate"](attributes): - continue - - if attributes[descriptor["key"]]: - # needs_word wrappers can only be turned off if - # preceded by non-whitespace - if (i >= 1 and not line[i - 1].isspace()) \ - or not descriptor["needs_word"]: - if text: - # strip leading and trailing spaces and - # compress consecutive spaces in inline - # code blocks - if descriptor["key"] == "code": - text = re.sub(r"\s+", " ", text.strip()) - substrings.append( - FormattedString(text, attributes.copy())) - text = "" - attributes[descriptor["key"]] = False - i = i + l - else: - text = text + line[i : i + l] - i = i + l - - # Must have a chance of closing this, and needs_word - # wrappers must be followed by non-whitespace - elif descriptor["last_index"] >= i + l and \ - (not line[i + l].isspace() or \ - not descriptor["needs_word"]): - if text: - substrings.append( - FormattedString(text, attributes.copy())) - text = "" - attributes[descriptor["key"]] = True - i = i + l - - else: - text = text + line[i : i + l] - i = i + l - - break - - else: - # No wrapper matched here (NOTE: cannot happen since all - # wrapper prefixes are also wrappers, but for completeness' - # sake) - text = text + line[i] - i = i + 1 - - # Normal text - else: - text = text + line[i] - i = i + 1 - - if text: - substrings.append(FormattedString(text, attributes)) - - return cls(substrings) - - @classmethod - def from_html(cls, html): - # type: (str) -> Formatted - parser = MatrixHtmlParser() - parser.feed(html) - return cls(parser.get_substrings()) - - def to_html(self): - def add_attribute(string, name, value): - if name == "bold" and value: - return "{bold_on}{text}{bold_off}".format( - bold_on="", text=string, bold_off="" - ) - if name == "italic" and value: - return "{italic_on}{text}{italic_off}".format( - italic_on="", text=string, italic_off="" - ) - if name == "underline" and value: - return "{underline_on}{text}{underline_off}".format( - underline_on="", text=string, underline_off="" - ) - if name == "strikethrough" and value: - return "{strike_on}{text}{strike_off}".format( - strike_on="", text=string, strike_off="" - ) - if name == "quote" and value: - return "{quote_on}{text}{quote_off}".format( - quote_on="
", - text=string, - quote_off="
", - ) - if name == "code" and value: - return "{code_on}{text}{code_off}".format( - code_on="", text=string, code_off="" - ) - - return string - - def add_color(string, fgcolor, bgcolor): - fgcolor_string = "" - bgcolor_string = "" - - if fgcolor: - fgcolor_string = " data-mx-color={}".format( - color_weechat_to_html(fgcolor) - ) - - if bgcolor: - bgcolor_string = " data-mx-bg-color={}".format( - color_weechat_to_html(bgcolor) - ) - - return "{color_on}{text}{color_off}".format( - color_on="".format( - fg=fgcolor_string, - bg=bgcolor_string - ), - text=string, - color_off="", - ) - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes.copy() - - # Escape HTML tag characters - text = text.replace("&", "&") \ - .replace("<", "<") \ - .replace(">", ">") - - if attributes["code"]: - if attributes["preformatted"]: - # XXX: This can't really happen since there's no way of - # creating preformatted code blocks in weechat (because - # there is not multiline input), but I'm creating this - # branch as a note that it should be handled once we do - # implement them. - pass - else: - text = add_attribute(text, "code", True) - attributes.pop("code") - - if attributes["fgcolor"] or attributes["bgcolor"]: - text = add_color( - text, - attributes["fgcolor"], - attributes["bgcolor"] - ) - - if attributes["fgcolor"]: - attributes.pop("fgcolor") - - if attributes["bgcolor"]: - attributes.pop("bgcolor") - - for key, value in attributes.items(): - text = add_attribute(text, key, value) - - return text - - html_string = map(format_string, self.substrings) - return "".join(html_string) - - # TODO do we want at least some formatting using unicode - # (strikethrough, quotes)? - def to_plain(self): - # type: () -> str - def strip_atribute(string, _, __): - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = strip_atribute(text, key, value) - return text - - plain_string = map(format_string, self.substrings) - return "".join(plain_string) - - def to_weechat(self): - def add_attribute(string, name, value, attributes): - if not value: - return string - elif name == "bold": - return "{bold_on}{text}{bold_off}".format( - bold_on=W.color("bold"), - text=string, - bold_off=W.color("-bold"), - ) - elif name == "italic": - return "{italic_on}{text}{italic_off}".format( - italic_on=W.color("italic"), - text=string, - italic_off=W.color("-italic"), - ) - elif name == "underline": - return "{underline_on}{text}{underline_off}".format( - underline_on=W.color("underline"), - text=string, - underline_off=W.color("-underline"), - ) - elif name == "strikethrough": - return string_strikethrough(string) - elif name == "quote": - quote_pair = color_pair(G.CONFIG.color.quote_fg, - G.CONFIG.color.quote_bg) - - # Remove leading and trailing newlines; Riot sends an extra - # quoted "\n" when a user quotes a message. - string = string.strip("\n") - if len(string) == 0: - return string - - if G.CONFIG.look.quote_wrap >= 0: - wrapper = self.textwrapper(G.CONFIG.look.quote_wrap, quote_pair) - return wrapper.fill(W.string_remove_color(string, "")) - else: - # Don't wrap, just add quote markers to all lines - return "{color_on}{text}{color_off}".format( - color_on=W.color(quote_pair), - text="> " + W.string_remove_color(string.replace("\n", "\n> "), ""), - color_off=W.color("resetcolor") - ) - elif name == "code": - code_color_pair = color_pair( - G.CONFIG.color.untagged_code_fg, - G.CONFIG.color.untagged_code_bg - ) - - margin = G.CONFIG.look.code_block_margin - - if attributes["preformatted"]: - # code block - - try: - lexer = get_lexer_by_name(value) - except ClassNotFound: - if G.CONFIG.look.code_blocks: - return colored_text_block( - string, - margin=margin, - color_pair=code_color_pair) - else: - return string_color_and_reset(string, - code_color_pair) - - try: - style = get_style_by_name(G.CONFIG.look.pygments_style) - except ClassNotFound: - style = "native" - - if G.CONFIG.look.code_blocks: - code_block = text_block(string, margin=margin) - else: - code_block = string - - # highlight adds a newline to the end of the string, remove - # it from the output - highlighted_code = highlight( - code_block, - lexer, - WeechatFormatter(style=style) - ).rstrip() - - return highlighted_code - else: - return string_color_and_reset(string, code_color_pair) - elif name == "fgcolor": - return "{color_on}{text}{color_off}".format( - color_on=W.color(value), - text=string, - color_off=W.color("resetcolor"), - ) - elif name == "bgcolor": - return "{color_on}{text}{color_off}".format( - color_on=W.color("," + value), - text=string, - color_off=W.color("resetcolor"), - ) - else: - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - # We need to handle strikethrough first, since doing - # a strikethrough followed by other attributes succeeds in the - # terminal, but doing it the other way around results in garbage. - if "strikethrough" in attributes: - text = add_attribute( - text, - "strikethrough", - attributes["strikethrough"], - attributes - ) - attributes.pop("strikethrough") - - def indent(text, prefix): - return prefix + text.replace("\n", "\n{}".format(prefix)) - - for key, value in attributes.items(): - if not value: - continue - - # Don't use textwrap to quote the code - if key == "quote" and attributes["code"]: - continue - - # Reflow inline code blocks - if key == "code" and not attributes["preformatted"]: - text = text.strip().replace('\n', ' ') - - text = add_attribute(text, key, value, attributes) - - # If we're quoted code add quotation marks now. - if key == "code" and attributes["quote"]: - fg = G.CONFIG.color.quote_fg - bg = G.CONFIG.color.quote_bg - text = indent( - text, - string_color_and_reset(">", color_pair(fg, bg)) + " ", - ) - - # If we're code don't remove multiple newlines blindly - if attributes["code"]: - return text - return re.sub(r"\n+", "\n", text) - - weechat_strings = map(format_string, self.substrings) - - # Remove duplicate \n elements from the list - strings = [] - for string in weechat_strings: - if len(strings) == 0 or string != "\n" or string != strings[-1]: - strings.append(string) - - return "".join(strings).strip() - - -DEFAULT_ATTRIBUTES = { - "bold": False, - "italic": False, - "underline": False, - "strikethrough": False, - "preformatted": False, - "quote": False, - "code": None, - "fgcolor": None, - "bgcolor": None, -} # type: Dict[str, Union[bool, Optional[str]]] - - -class MatrixHtmlParser(HTMLParser): - # TODO bullets - def __init__(self): - HTMLParser.__init__(self) - self.text = "" # type: str - self.substrings = [] # type: List[FormattedString] - self.attributes = DEFAULT_ATTRIBUTES.copy() - - def unescape(self, text): - """Shim to unescape HTML in both Python 2 and 3. - - The instance method was deprecated in Python 3 and html.unescape - doesn't exist in Python 2 so this is needed. - """ - try: - return html.unescape(text) - except AttributeError: - return HTMLParser.unescape(self, text) - - def add_substring(self, text, attrs): - fmt_string = FormattedString(text, attrs) - self.substrings.append(fmt_string) - - def _toggle_attribute(self, attribute): - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes[attribute] = not self.attributes[attribute] - - def handle_starttag(self, tag, attrs): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "pre": - self._toggle_attribute("preformatted") - elif tag == "code": - lang = None - - for key, value in attrs: - if key == "class": - if value.startswith("language-"): - lang = value.split("-", 1)[1] - - lang = lang or "unknown" - - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["code"] = lang - elif tag == "p": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "\n" - self.add_substring(self.text, DEFAULT_ATTRIBUTES.copy()) - self.text = "" - elif tag == "br": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "\n" - self.add_substring(self.text, DEFAULT_ATTRIBUTES.copy()) - self.text = "" - elif tag == "font": - for key, value in attrs: - if key in ["data-mx-color", "color"]: - color = color_html_to_weechat(value) - - if not color: - continue - - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["fgcolor"] = color - - elif key in ["data-mx-bg-color"]: - color = color_html_to_weechat(value) - if not color: - continue - - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["bgcolor"] = color - - else: - pass - - def handle_endtag(self, tag): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "pre": - self._toggle_attribute("preformatted") - elif tag == "code": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["code"] = None - elif tag == "blockquote": - self._toggle_attribute("quote") - self.text = "\n" - self.add_substring(self.text, DEFAULT_ATTRIBUTES.copy()) - self.text = "" - elif tag == "font": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["fgcolor"] = None - else: - pass - - def handle_data(self, data): - self.text += data - - def handle_entityref(self, name): - self.text += self.unescape("&{};".format(name)) - - def handle_charref(self, name): - self.text += self.unescape("&#{};".format(name)) - - def get_substrings(self): - if self.text: - self.add_substring(self.text, self.attributes.copy()) - - return self.substrings - - -def color_line_to_weechat(color_string): - # type: (str) -> str - line_colors = { - "0": "white", - "1": "black", - "2": "blue", - "3": "green", - "4": "lightred", - "5": "red", - "6": "magenta", - "7": "brown", - "8": "yellow", - "9": "lightgreen", - "10": "cyan", - "11": "lightcyan", - "12": "lightblue", - "13": "lightmagenta", - "14": "darkgray", - "15": "gray", - "16": "52", - "17": "94", - "18": "100", - "19": "58", - "20": "22", - "21": "29", - "22": "23", - "23": "24", - "24": "17", - "25": "54", - "26": "53", - "27": "89", - "28": "88", - "29": "130", - "30": "142", - "31": "64", - "32": "28", - "33": "35", - "34": "30", - "35": "25", - "36": "18", - "37": "91", - "38": "90", - "39": "125", - "40": "124", - "41": "166", - "42": "184", - "43": "106", - "44": "34", - "45": "49", - "46": "37", - "47": "33", - "48": "19", - "49": "129", - "50": "127", - "51": "161", - "52": "196", - "53": "208", - "54": "226", - "55": "154", - "56": "46", - "57": "86", - "58": "51", - "59": "75", - "60": "21", - "61": "171", - "62": "201", - "63": "198", - "64": "203", - "65": "215", - "66": "227", - "67": "191", - "68": "83", - "69": "122", - "70": "87", - "71": "111", - "72": "63", - "73": "177", - "74": "207", - "75": "205", - "76": "217", - "77": "223", - "78": "229", - "79": "193", - "80": "157", - "81": "158", - "82": "159", - "83": "153", - "84": "147", - "85": "183", - "86": "219", - "87": "212", - "88": "16", - "89": "233", - "90": "235", - "91": "237", - "92": "239", - "93": "241", - "94": "244", - "95": "247", - "96": "250", - "97": "254", - "98": "231", - "99": "default", - } - - assert color_string in line_colors - - return line_colors[color_string] - - -# The functions color_dist_sq(), color_to_6cube(), and color_find_rgb -# are python ports of the same named functions from the tmux -# source, they are under the copyright of Nicholas Marriott, and Avi Halachmi -# under the ISC license. -# More info: https://github.com/tmux/tmux/blob/master/colour.c - - -def color_dist_sq(R, G, B, r, g, b): - # pylint: disable=invalid-name,too-many-arguments - # type: (int, int, int, int, int, int) -> int - return (R - r) * (R - r) + (G - g) * (G - g) + (B - b) * (B - b) - - -def color_to_6cube(v): - # pylint: disable=invalid-name - # type: (int) -> int - if v < 48: - return 0 - if v < 114: - return 1 - return (v - 35) // 40 - - -def color_find_rgb(r, g, b): - # type: (int, int, int) -> int - """Convert an RGB triplet to the xterm(1) 256 color palette. - - xterm provides a 6x6x6 color cube (16 - 231) and 24 greys (232 - 255). - We map our RGB color to the closest in the cube, also work out the - closest grey, and use the nearest of the two. - - Note that the xterm has much lower resolution for darker colors (they - are not evenly spread out), so our 6 levels are not evenly spread: 0x0, - 0x5f (95), 0x87 (135), 0xaf (175), 0xd7 (215) and 0xff (255). Greys are - more evenly spread (8, 18, 28 ... 238). - """ - # pylint: disable=invalid-name - q2c = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] - - # Map RGB to 6x6x6 cube. - qr = color_to_6cube(r) - qg = color_to_6cube(g) - qb = color_to_6cube(b) - - cr = q2c[qr] - cg = q2c[qg] - cb = q2c[qb] - - # If we have hit the color exactly, return early. - if cr == r and cg == g and cb == b: - return 16 + (36 * qr) + (6 * qg) + qb - - # Work out the closest grey (average of RGB). - grey_avg = (r + g + b) // 3 - - if grey_avg > 238: - grey_idx = 23 - else: - grey_idx = (grey_avg - 3) // 10 - - grey = 8 + (10 * grey_idx) - - # Is grey or 6x6x6 color closest? - d = color_dist_sq(cr, cg, cb, r, g, b) - - if color_dist_sq(grey, grey, grey, r, g, b) < d: - idx = 232 + grey_idx - else: - idx = 16 + (36 * qr) + (6 * qg) + qb - - return idx - - -def color_html_to_weechat(color): - # type: (str) -> str - # yapf: disable - weechat_basic_colors = { - (0, 0, 0): "black", # 0 - (128, 0, 0): "red", # 1 - (0, 128, 0): "green", # 2 - (128, 128, 0): "brown", # 3 - (0, 0, 128): "blue", # 4 - (128, 0, 128): "magenta", # 5 - (0, 128, 128): "cyan", # 6 - (192, 192, 192): "default", # 7 - (128, 128, 128): "gray", # 8 - (255, 0, 0): "lightred", # 9 - (0, 255, 0): "lightgreen", # 10 - (255, 255, 0): "yellow", # 11 - (0, 0, 255): "lightblue", # 12 - (255, 0, 255): "lightmagenta", # 13 - (0, 255, 255): "lightcyan", # 14 - (255, 255, 255): "white", # 15 - } - # yapf: enable - - try: - rgb_color = webcolors.html5_parse_legacy_color(color) - except ValueError: - return "" - - if rgb_color in weechat_basic_colors: - return weechat_basic_colors[rgb_color] - - return str(color_find_rgb(*rgb_color)) - - -def color_weechat_to_html(color): - # type: (str) -> str - # yapf: disable - weechat_basic_colors = { - "black": "0", - "red": "1", - "green": "2", - "brown": "3", - "blue": "4", - "magenta": "5", - "cyan": "6", - "default": "7", - "gray": "8", - "lightred": "9", - "lightgreen": "10", - "yellow": "11", - "lightblue": "12", - "lightmagenta": "13", - "lightcyan": "14", - "white": "15", - } - hex_colors = { - "0": "#000000", - "1": "#800000", - "2": "#008000", - "3": "#808000", - "4": "#000080", - "5": "#800080", - "6": "#008080", - "7": "#c0c0c0", - "8": "#808080", - "9": "#ff0000", - "10": "#00ff00", - "11": "#ffff00", - "12": "#0000ff", - "13": "#ff00ff", - "14": "#00ffff", - "15": "#ffffff", - "16": "#000000", - "17": "#00005f", - "18": "#000087", - "19": "#0000af", - "20": "#0000d7", - "21": "#0000ff", - "22": "#005f00", - "23": "#005f5f", - "24": "#005f87", - "25": "#005faf", - "26": "#005fd7", - "27": "#005fff", - "28": "#008700", - "29": "#00875f", - "30": "#008787", - "31": "#0087af", - "32": "#0087d7", - "33": "#0087ff", - "34": "#00af00", - "35": "#00af5f", - "36": "#00af87", - "37": "#00afaf", - "38": "#00afd7", - "39": "#00afff", - "40": "#00d700", - "41": "#00d75f", - "42": "#00d787", - "43": "#00d7af", - "44": "#00d7d7", - "45": "#00d7ff", - "46": "#00ff00", - "47": "#00ff5f", - "48": "#00ff87", - "49": "#00ffaf", - "50": "#00ffd7", - "51": "#00ffff", - "52": "#5f0000", - "53": "#5f005f", - "54": "#5f0087", - "55": "#5f00af", - "56": "#5f00d7", - "57": "#5f00ff", - "58": "#5f5f00", - "59": "#5f5f5f", - "60": "#5f5f87", - "61": "#5f5faf", - "62": "#5f5fd7", - "63": "#5f5fff", - "64": "#5f8700", - "65": "#5f875f", - "66": "#5f8787", - "67": "#5f87af", - "68": "#5f87d7", - "69": "#5f87ff", - "70": "#5faf00", - "71": "#5faf5f", - "72": "#5faf87", - "73": "#5fafaf", - "74": "#5fafd7", - "75": "#5fafff", - "76": "#5fd700", - "77": "#5fd75f", - "78": "#5fd787", - "79": "#5fd7af", - "80": "#5fd7d7", - "81": "#5fd7ff", - "82": "#5fff00", - "83": "#5fff5f", - "84": "#5fff87", - "85": "#5fffaf", - "86": "#5fffd7", - "87": "#5fffff", - "88": "#870000", - "89": "#87005f", - "90": "#870087", - "91": "#8700af", - "92": "#8700d7", - "93": "#8700ff", - "94": "#875f00", - "95": "#875f5f", - "96": "#875f87", - "97": "#875faf", - "98": "#875fd7", - "99": "#875fff", - "100": "#878700", - "101": "#87875f", - "102": "#878787", - "103": "#8787af", - "104": "#8787d7", - "105": "#8787ff", - "106": "#87af00", - "107": "#87af5f", - "108": "#87af87", - "109": "#87afaf", - "110": "#87afd7", - "111": "#87afff", - "112": "#87d700", - "113": "#87d75f", - "114": "#87d787", - "115": "#87d7af", - "116": "#87d7d7", - "117": "#87d7ff", - "118": "#87ff00", - "119": "#87ff5f", - "120": "#87ff87", - "121": "#87ffaf", - "122": "#87ffd7", - "123": "#87ffff", - "124": "#af0000", - "125": "#af005f", - "126": "#af0087", - "127": "#af00af", - "128": "#af00d7", - "129": "#af00ff", - "130": "#af5f00", - "131": "#af5f5f", - "132": "#af5f87", - "133": "#af5faf", - "134": "#af5fd7", - "135": "#af5fff", - "136": "#af8700", - "137": "#af875f", - "138": "#af8787", - "139": "#af87af", - "140": "#af87d7", - "141": "#af87ff", - "142": "#afaf00", - "143": "#afaf5f", - "144": "#afaf87", - "145": "#afafaf", - "146": "#afafd7", - "147": "#afafff", - "148": "#afd700", - "149": "#afd75f", - "150": "#afd787", - "151": "#afd7af", - "152": "#afd7d7", - "153": "#afd7ff", - "154": "#afff00", - "155": "#afff5f", - "156": "#afff87", - "157": "#afffaf", - "158": "#afffd7", - "159": "#afffff", - "160": "#d70000", - "161": "#d7005f", - "162": "#d70087", - "163": "#d700af", - "164": "#d700d7", - "165": "#d700ff", - "166": "#d75f00", - "167": "#d75f5f", - "168": "#d75f87", - "169": "#d75faf", - "170": "#d75fd7", - "171": "#d75fff", - "172": "#d78700", - "173": "#d7875f", - "174": "#d78787", - "175": "#d787af", - "176": "#d787d7", - "177": "#d787ff", - "178": "#d7af00", - "179": "#d7af5f", - "180": "#d7af87", - "181": "#d7afaf", - "182": "#d7afd7", - "183": "#d7afff", - "184": "#d7d700", - "185": "#d7d75f", - "186": "#d7d787", - "187": "#d7d7af", - "188": "#d7d7d7", - "189": "#d7d7ff", - "190": "#d7ff00", - "191": "#d7ff5f", - "192": "#d7ff87", - "193": "#d7ffaf", - "194": "#d7ffd7", - "195": "#d7ffff", - "196": "#ff0000", - "197": "#ff005f", - "198": "#ff0087", - "199": "#ff00af", - "200": "#ff00d7", - "201": "#ff00ff", - "202": "#ff5f00", - "203": "#ff5f5f", - "204": "#ff5f87", - "205": "#ff5faf", - "206": "#ff5fd7", - "207": "#ff5fff", - "208": "#ff8700", - "209": "#ff875f", - "210": "#ff8787", - "211": "#ff87af", - "212": "#ff87d7", - "213": "#ff87ff", - "214": "#ffaf00", - "215": "#ffaf5f", - "216": "#ffaf87", - "217": "#ffafaf", - "218": "#ffafd7", - "219": "#ffafff", - "220": "#ffd700", - "221": "#ffd75f", - "222": "#ffd787", - "223": "#ffd7af", - "224": "#ffd7d7", - "225": "#ffd7ff", - "226": "#ffff00", - "227": "#ffff5f", - "228": "#ffff87", - "229": "#ffffaf", - "230": "#ffffd7", - "231": "#ffffff", - "232": "#080808", - "233": "#121212", - "234": "#1c1c1c", - "235": "#262626", - "236": "#303030", - "237": "#3a3a3a", - "238": "#444444", - "239": "#4e4e4e", - "240": "#585858", - "241": "#626262", - "242": "#6c6c6c", - "243": "#767676", - "244": "#808080", - "245": "#8a8a8a", - "246": "#949494", - "247": "#9e9e9e", - "248": "#a8a8a8", - "249": "#b2b2b2", - "250": "#bcbcbc", - "251": "#c6c6c6", - "252": "#d0d0d0", - "253": "#dadada", - "254": "#e4e4e4", - "255": "#eeeeee" - } - - # yapf: enable - if color in weechat_basic_colors: - return hex_colors[weechat_basic_colors[color]] - return hex_colors[color] - - -class WeechatFormatter(Formatter): - def __init__(self, **options): - Formatter.__init__(self, **options) - self.styles = {} - - for token, style in self.style: - start = end = "" - if style["color"]: - start += "{}".format( - W.color(color_html_to_weechat(str(style["color"]))) - ) - end = "{}".format(W.color("resetcolor")) + end - if style["bold"]: - start += W.color("bold") - end = W.color("-bold") + end - if style["italic"]: - start += W.color("italic") - end = W.color("-italic") + end - if style["underline"]: - start += W.color("underline") - end = W.color("-underline") + end - self.styles[token] = (start, end) - - def format(self, tokensource, outfile): - lastval = "" - lasttype = None - - for ttype, value in tokensource: - while ttype not in self.styles: - ttype = ttype.parent - - if ttype == lasttype: - lastval += value - else: - if lastval: - stylebegin, styleend = self.styles[lasttype] - outfile.write(stylebegin + lastval + styleend) - # set lastval/lasttype to current values - lastval = value - lasttype = ttype - - if lastval: - stylebegin, styleend = self.styles[lasttype] - outfile.write(stylebegin + lastval + styleend) diff --git a/weechat/python/matrix/commands.py b/weechat/python/matrix/commands.py deleted file mode 100644 index 53faf74..0000000 --- a/weechat/python/matrix/commands.py +++ /dev/null @@ -1,1969 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals -import argparse -import os -import re -from builtins import str -from future.moves.itertools import zip_longest -from collections import defaultdict -from functools import partial -from nio import EncryptionError, LocalProtocolError - -from . import globals as G -from .colors import Formatted -from .globals import SERVERS, W, UPLOADS, SCRIPT_NAME -from .server import MatrixServer -from .utf import utf8_decode -from .utils import key_from_value, parse_redact_args -from .uploads import UploadsBuffer, Upload - -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse # type: ignore - - -class ParseError(Exception): - pass - - -class WeechatArgParse(argparse.ArgumentParser): - def print_usage(self, file=None): - pass - - def error(self, message): - message = ( - "{prefix}Error: {message} for command {command} " - "(see /help {command})" - ).format(prefix=W.prefix("error"), message=message, command=self.prog) - W.prnt("", message) - raise ParseError - - -class WeechatCommandParser(object): - @staticmethod - def _run_parser(parser, args): - try: - parsed_args = parser.parse_args(args.split()) - return parsed_args - except ParseError: - return None - - @staticmethod - def topic(args): - parser = WeechatArgParse(prog="topic") - - parser.add_argument("-delete", action="store_true") - parser.add_argument("topic", nargs="*") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def kick(args): - parser = WeechatArgParse(prog="kick") - parser.add_argument("user_id") - parser.add_argument("reason", nargs="*") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def invite(args): - parser = WeechatArgParse(prog="invite") - parser.add_argument("user_id") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def join(args): - parser = WeechatArgParse(prog="join") - parser.add_argument("room_id") - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def part(args): - parser = WeechatArgParse(prog="part") - parser.add_argument("room_id", nargs="?") - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def devices(args): - parser = WeechatArgParse(prog="devices") - subparsers = parser.add_subparsers(dest="subcommand") - subparsers.add_parser("list") - - delete_parser = subparsers.add_parser("delete") - delete_parser.add_argument("device_id") - - name_parser = subparsers.add_parser("set-name") - name_parser.add_argument("device_id") - name_parser.add_argument("device_name", nargs="*") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def olm(args): - parser = WeechatArgParse(prog="olm") - subparsers = parser.add_subparsers(dest="subcommand") - - info_parser = subparsers.add_parser("info") - info_parser.add_argument( - "category", nargs="?", default="private", - choices=[ - "all", - "blacklisted", - "private", - "unverified", - "verified", - "ignored" - ]) - info_parser.add_argument("filter", nargs="?") - - verify_parser = subparsers.add_parser("verify") - verify_parser.add_argument("user_filter") - verify_parser.add_argument("device_filter", nargs="?") - - unverify_parser = subparsers.add_parser("unverify") - unverify_parser.add_argument("user_filter") - unverify_parser.add_argument("device_filter", nargs="?") - - blacklist_parser = subparsers.add_parser("blacklist") - blacklist_parser.add_argument("user_filter") - blacklist_parser.add_argument("device_filter", nargs="?") - - unblacklist_parser = subparsers.add_parser("unblacklist") - unblacklist_parser.add_argument("user_filter") - unblacklist_parser.add_argument("device_filter", nargs="?") - - ignore_parser = subparsers.add_parser("ignore") - ignore_parser.add_argument("user_filter") - ignore_parser.add_argument("device_filter", nargs="?") - - unignore_parser = subparsers.add_parser("unignore") - unignore_parser.add_argument("user_filter") - unignore_parser.add_argument("device_filter", nargs="?") - - export_parser = subparsers.add_parser("export") - export_parser.add_argument("file") - export_parser.add_argument("passphrase") - - import_parser = subparsers.add_parser("import") - import_parser.add_argument("file") - import_parser.add_argument("passphrase") - - sas_parser = subparsers.add_parser("verification") - sas_parser.add_argument( - "action", - choices=[ - "start", - "accept", - "confirm", - "cancel", - ]) - sas_parser.add_argument("user_id") - sas_parser.add_argument("device_id") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def room(args): - parser = WeechatArgParse(prog="room") - subparsers = parser.add_subparsers(dest="subcommand") - typing_notification = subparsers.add_parser("typing-notifications") - typing_notification.add_argument( - "state", - choices=["enable", "disable", "toggle"] - ) - - read_markers = subparsers.add_parser("read-markers") - read_markers.add_argument( - "state", - choices=["enable", "disable", "toggle"] - ) - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def uploads(args): - parser = WeechatArgParse(prog="uploads") - subparsers = parser.add_subparsers(dest="subcommand") - subparsers.add_parser("list") - subparsers.add_parser("listfull") - subparsers.add_parser("up") - subparsers.add_parser("down") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def upload(args): - parser = WeechatArgParse(prog="upload") - parser.add_argument("file") - return WeechatCommandParser._run_parser(parser, args) - - -def grouper(iterable, n, fillvalue=None): - "Collect data into fixed-length chunks or blocks" - # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" - args = [iter(iterable)] * n - return zip_longest(*args, fillvalue=fillvalue) - - -def partition_key(key): - groups = grouper(key, 4, " ") - return ' '.join(''.join(g) for g in groups) - - -def hook_commands(): - W.hook_command( - # Command name and short description - "matrix", - "Matrix chat protocol command", - # Synopsis - ( - "server add [:] ||" - "server delete|list|listfull ||" - "connect ||" - "disconnect ||" - "reconnect ||" - "help " - ), - # Description - ( - " server: list, add, or remove Matrix servers\n" - " connect: connect to Matrix servers\n" - "disconnect: disconnect from one or all Matrix servers\n" - " reconnect: reconnect to server(s)\n" - " help: show detailed command help\n\n" - "Use /matrix help [command] to find out more.\n" - ), - # Completions - ( - "server %(matrix_server_commands)|%* ||" - "connect %(matrix_servers) ||" - "disconnect %(matrix_servers) ||" - "reconnect %(matrix_servers) ||" - "help %(matrix_commands)" - ), - # Function name - "matrix_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "redact", - "redact messages", - # Synopsis - ('[:""] []'), - # Description - ( - " event-id: event id of the message that will be redacted\n" - "message-part: an initial part of the message (ignored, only " - "used\n" - " as visual feedback when using completion)\n" - " reason: the redaction reason\n" - ), - # Completions - ("%(matrix_messages)"), - # Function name - "matrix_redact_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "reply-matrix", - "reply to a message", - # Synopsis - ('[:""] []'), - # Description - ( - " event-id: event id of the message that will be replied to\n" - "message-part: an initial part of the message (ignored, only " - "used\n" - " as visual feedback when using completion)\n" - " reply: the reply\n" - ), - # Completions - ("%(matrix_messages)"), - # Function name - "matrix_reply_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "topic", - "get/set the room topic", - # Synopsis - ("[|-delete]"), - # Description - (" topic: topic to set\n" "-delete: delete room topic"), - # Completions - "", - # Callback - "matrix_topic_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "me", - "send an emote message to the current room", - # Synopsis - (""), - # Description - ("message: message to send"), - # Completions - "", - # Callback - "matrix_me_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "kick", - "kick a user from the current room", - # Synopsis - (" []"), - # Description - ( - "user-id: user-id to kick\n" - " reason: reason why the user was kicked" - ), - # Completions - ("%(matrix_users)"), - # Callback - "matrix_kick_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "invite", - "invite a user to the current room", - # Synopsis - (""), - # Description - ("user-id: user-id to invite"), - # Completions - ("%(matrix_users)"), - # Callback - "matrix_invite_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "join", - "join a room", - # Synopsis - ("|"), - # Description - ( - " room-id: room-id of the room to join\n" - "room-alias: room alias of the room to join" - ), - # Completions - "", - # Callback - "matrix_join_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "part", - "leave a room", - # Synopsis - ("[]"), - # Description - (" room-name: room name of the room to leave"), - # Completions - "", - # Callback - "matrix_part_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "devices", - "list, delete or rename matrix devices", - # Synopsis - ("list ||" - "delete ||" - "set-name " - ), - # Description - ("device-id: device id of the device to delete\n" - " name: new device name to set\n"), - # Completions - ("list ||" - "delete %(matrix_own_devices) ||" - "set-name %(matrix_own_devices)"), - # Callback - "matrix_devices_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "olm", - "Matrix olm encryption configuration command", - # Synopsis - ("info all|blacklisted|ignored|private|unverified|verified ||" - "blacklist ||" - "unverify ||" - "verify ||" - "verification start|accept|cancel|confirm ||" - "ignore ||" - "unignore ||" - "export ||" - "import " - ), - # Description - (" info: show info about known devices and their keys\n" - " blacklist: blacklist a device\n" - "unblacklist: unblacklist a device\n" - " unverify: unverify a device\n" - " verify: verify a device\n" - " ignore: ignore an unverifiable but non-blacklist-worthy device\n" - " unignore: unignore a device\n" - "verification: manage interactive device verification\n" - " export: export encryption keys\n" - " import: import encryption keys\n\n" - "Examples:" - "\n /olm verify @example:example.com *" - "\n /olm info all example*" - ), - # Completions - ('info all|blacklisted|ignored|private|unverified|verified ||' - 'blacklist %(olm_user_ids) %(olm_devices) ||' - 'unblacklist %(olm_user_ids) %(olm_devices) ||' - 'unverify %(olm_user_ids) %(olm_devices) ||' - 'verify %(olm_user_ids) %(olm_devices) ||' - 'verification start|accept|cancel|confirm %(olm_user_ids) %(olm_devices) ||' - 'ignore %(olm_user_ids) %(olm_devices) ||' - 'unignore %(olm_user_ids) %(olm_devices) ||' - 'export %(filename) ||' - 'import %(filename)' - ), - # Function name - 'matrix_olm_command_cb', - '') - - W.hook_command( - # Command name and short description - "room", - "change room state", - # Synopsis - ("typing-notifications ||" - "read-markers " - ), - # Description - ("state: one of enable, disable or toggle\n"), - # Completions - ("typing-notifications enable|disable|toggle||" - "read-markers enable|disable|toggle" - ), - # Callback - "matrix_room_command_cb", - "", - ) - - # W.hook_command( - # # Command name and short description - # "uploads", - # "Open the uploads buffer or list uploads in the core buffer", - # # Synopsis - # ("list||" - # "listfull" - # ), - # # Description - # (""), - # # Completions - # ("list ||" - # "listfull"), - # # Callback - # "matrix_uploads_command_cb", - # "", - # ) - - W.hook_command( - # Command name and short description - "upload", - "Upload a file to a room", - # Synopsis - (""), - # Description - (""), - # Completions - ("%(filename)"), - # Callback - "matrix_upload_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "send-anyways", - "Send the last message in a room ignorin unverified devices.", - # Synopsis - "", - # Description - "Send the last message in a room despite there being unverified " - "devices. The unverified devices will be marked as ignored after " - "running this command.", - # Completions - "", - # Callback - "matrix_send_anyways_cb", - "", - ) - - W.hook_command_run("/buffer clear", "matrix_command_buf_clear_cb", "") - - if G.CONFIG.network.fetch_backlog_on_pgup: - hook_page_up() - - -def hook_key_bindings(): - W.hook_hsignal("matrix_cursor_reply", "matrix_cursor_reply_signal_cb", "") - - binding = "@chat(python.{}*):r".format(G.BUFFER_NAME_PREFIX) - W.key_bind("cursor", { - binding: "hsignal:matrix_cursor_reply", - }) - - -def format_device(device_id, fp_key, display_name): - fp_key = partition_key(fp_key) - message = (" - Device ID: {device_color}{device_id}{ncolor}\n" - " - Display name: {device_color}{display_name}{ncolor}\n" - " - Device key: {key_color}{fp_key}{ncolor}").format( - device_color=W.color("chat_channel"), - device_id=device_id, - ncolor=W.color("reset"), - display_name=display_name, - key_color=W.color("chat_server"), - fp_key=fp_key) - return message - - -def olm_info_command(server, args): - def print_devices( - device_store, - filter_regex, - device_category="All", - predicate=None, - ): - user_strings = [] - try: - filter_regex = re.compile(args.filter) if args.filter else None - except re.error as e: - server.error("Invalid regular expression: {}.".format(e.args[0])) - return - - for user_id in sorted(device_store.users): - device_strings = [] - for device in device_store.active_user_devices(user_id): - if filter_regex: - if (not filter_regex.search(user_id) and - not filter_regex.search(device.id)): - continue - - if predicate: - if not predicate(device): - continue - - device_strings.append(format_device( - device.id, - device.ed25519, - device.display_name - )) - - if not device_strings: - continue - - d_string = "\n".join(device_strings) - message = (" - User: {user_color}{user}{ncolor}\n").format( - user_color=W.color("chat_nick"), - user=user_id, - ncolor=W.color("reset")) - message += d_string - user_strings.append(message) - - if not user_strings: - message = ("{prefix}matrix: No matching devices " - "found.").format(prefix=W.prefix("error")) - W.prnt(server.server_buffer, message) - return - - server.info("{} devices:\n".format(device_category)) - W.prnt(server.server_buffer, "\n".join(user_strings)) - - olm = server.client.olm - - if not hasattr(args, 'category') or args.category == "private": - fp_key = partition_key(olm.account.identity_keys["ed25519"]) - message = ("Identity keys:\n" - " - User: {user_color}{user}{ncolor}\n" - " - Device ID: {device_color}{device_id}{ncolor}\n" - " - Device key: {key_color}{fp_key}{ncolor}\n" - "").format( - user_color=W.color("chat_self"), - ncolor=W.color("reset"), - user=olm.user_id, - device_color=W.color("chat_channel"), - device_id=olm.device_id, - key_color=W.color("chat_server"), - fp_key=fp_key) - server.info(message) - - elif args.category == "all": - print_devices(olm.device_store, args.filter) - - elif args.category == "verified": - print_devices( - olm.device_store, - args.filter, - "Verified", - olm.is_device_verified - ) - - elif args.category == "unverified": - def predicate(device): - return not olm.is_device_verified(device) - - print_devices( - olm.device_store, - args.filter, - "Unverified", - predicate - ) - - elif args.category == "blacklisted": - print_devices( - olm.device_store, - args.filter, - "Blacklisted", - olm.is_device_blacklisted - ) - - elif args.category == "ignored": - print_devices( - olm.device_store, - args.filter, - "Ignored", - olm.is_device_ignored - ) - - -def olm_action_command(server, args, category, error_category, prefix, action): - device_store = server.client.olm.device_store - users = [] - - if args.user_filter == "*": - users = device_store.users - else: - users = [x for x in device_store.users if args.user_filter in x] - - user_devices = { - user: device_store.active_user_devices(user) for user in users - } - - if args.device_filter and args.device_filter != "*": - filtered_user_devices = {} - for user, device_list in user_devices.items(): - filtered_devices = filter( - lambda x: args.device_filter in x.id, - device_list - ) - filtered_user_devices[user] = list(filtered_devices) - user_devices = filtered_user_devices - - changed_devices = defaultdict(list) - - for user, device_list in user_devices.items(): - for device in device_list: - if action(device): - changed_devices[user].append(device) - - if not changed_devices: - message = ("{prefix}matrix: No matching {error_category} devices " - "found.").format( - prefix=W.prefix("error"), - error_category=error_category - ) - W.prnt(server.server_buffer, message) - return - - user_strings = [] - for user_id, device_list in changed_devices.items(): - device_strings = [] - message = (" - User: {user_color}{user}{ncolor}\n").format( - user_color=W.color("chat_nick"), - user=user_id, - ncolor=W.color("reset")) - for device in device_list: - device_strings.append(format_device( - device.id, - device.ed25519, - device.display_name - )) - if not device_strings: - continue - - d_string = "\n".join(device_strings) - message += d_string - user_strings.append(message) - - W.prnt(server.server_buffer, - "{}matrix: {} key(s):\n".format(W.prefix("prefix"), category)) - W.prnt(server.server_buffer, "\n".join(user_strings)) - pass - - -def olm_verify_command(server, args): - olm_action_command( - server, - args, - "Verified", - "unverified", - "join", - server.client.verify_device - ) - - -def olm_unverify_command(server, args): - olm_action_command( - server, - args, - "Unverified", - "verified", - "quit", - server.client.unverify_device - ) - - -def olm_blacklist_command(server, args): - olm_action_command( - server, - args, - "Blacklisted", - "unblacklisted", - "join", - server.client.blacklist_device - ) - - -def olm_unblacklist_command(server, args): - olm_action_command( - server, - args, - "Unblacklisted", - "blacklisted", - "join", - server.client.unblacklist_device - ) - - -def olm_ignore_command(server, args): - olm_action_command( - server, - args, - "Ignored", - "ignored", - "join", - server.client.ignore_device - ) - - -def olm_unignore_command(server, args): - olm_action_command( - server, - args, - "Unignored", - "unignored", - "join", - server.client.unignore_device - ) - - -def olm_export_command(server, args): - file_path = os.path.expanduser(args.file) - try: - server.client.export_keys(file_path, args.passphrase) - except (OSError, IOError) as e: - server.error("Error exporting keys: {}".format(str(e))) - - server.info("Successfully exported keys") - -def olm_import_command(server, args): - file_path = os.path.expanduser(args.file) - try: - server.client.import_keys(file_path, args.passphrase) - except (OSError, IOError, EncryptionError) as e: - server.error("Error importing keys: {}".format(str(e))) - - server.info("Successfully imported keys") - - -def olm_sas_command(server, args): - try: - device_store = server.client.device_store - except LocalProtocolError: - server.error("The device store is not loaded") - return W.WEECHAT_RC_OK - - try: - device = device_store[args.user_id][args.device_id] - except KeyError: - server.error("Device {} of user {} not found".format( - args.device_id, - args.user_id - )) - return W.WEECHAT_RC_OK - - if device.deleted: - server.error("Device {} of user {} is deleted.".format( - args.device_id, - args.user_id - )) - return W.WEECHAT_RC_OK - - if args.action == "start": - server.start_verification(device) - elif args.action in ["accept", "confirm", "cancel"]: - sas = server.client.get_active_sas(args.user_id, args.device_id) - - if not sas: - server.error("No active key verification found for " - "device {} of user {}.".format( - args.device_id, - args.user_id - )) - return W.WEECHAT_RC_OK - - try: - if args.action == "accept": - server.accept_sas(sas) - elif args.action == "confirm": - server.confirm_sas(sas) - elif args.action == "cancel": - server.cancel_sas(sas) - - except LocalProtocolError as e: - server.error(str(e)) - - -@utf8_decode -def matrix_olm_command_cb(data, buffer, args): - def command(server, data, buffer, args): - parsed_args = WeechatCommandParser.olm(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - if not server.client.olm: - W.prnt(server.server_buffer, "{}matrix: Olm account isn't " - "loaded.".format(W.prefix("error"))) - return W.WEECHAT_RC_OK - - if not parsed_args.subcommand or parsed_args.subcommand == "info": - olm_info_command(server, parsed_args) - elif parsed_args.subcommand == "export": - olm_export_command(server, parsed_args) - elif parsed_args.subcommand == "import": - olm_import_command(server, parsed_args) - elif parsed_args.subcommand == "verify": - olm_verify_command(server, parsed_args) - elif parsed_args.subcommand == "unverify": - olm_unverify_command(server, parsed_args) - elif parsed_args.subcommand == "blacklist": - olm_blacklist_command(server, parsed_args) - elif parsed_args.subcommand == "unblacklist": - olm_unblacklist_command(server, parsed_args) - elif parsed_args.subcommand == "verification": - olm_sas_command(server, parsed_args) - elif parsed_args.subcommand == "ignore": - olm_ignore_command(server, parsed_args) - elif parsed_args.subcommand == "unignore": - olm_unignore_command(server, parsed_args) - else: - message = ("{prefix}matrix: Command not implemented.".format( - prefix=W.prefix("error"))) - W.prnt(server.server_buffer, message) - - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - return command(server, data, buffer, args) - elif buffer == server.server_buffer: - return command(server, data, buffer, args) - - W.prnt("", "{prefix}matrix: command \"olm\" must be executed on a " - "matrix buffer (server or channel)".format( - prefix=W.prefix("error") - )) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_devices_command_cb(data, buffer, args): - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - parsed_args = WeechatCommandParser.devices(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - if not parsed_args.subcommand or parsed_args.subcommand == "list": - server.devices() - elif parsed_args.subcommand == "delete": - server.delete_device(parsed_args.device_id) - elif parsed_args.subcommand == "set-name": - new_name = " ".join(parsed_args.device_name).strip("\"") - server.rename_device(parsed_args.device_id, new_name) - - return W.WEECHAT_RC_OK - - W.prnt("", "{prefix}matrix: command \"devices\" must be executed on a " - "matrix buffer (server or channel)".format( - prefix=W.prefix("error") - )) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_me_command_cb(data, buffer, args): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - - if not server.connected: - message = ( - "{prefix}matrix: you are not connected to " "the server" - ).format(prefix=W.prefix("error")) - W.prnt(server.server_buffer, message) - return W.WEECHAT_RC_ERROR - - room_buffer = server.find_room_from_ptr(buffer) - - if not server.client.logged_in: - room_buffer.error("You are not logged in.") - return W.WEECHAT_RC_ERROR - - if not args: - return W.WEECHAT_RC_OK - - formatted_data = Formatted.from_input_line(args) - - server.room_send_message(room_buffer, formatted_data, "m.emote") - return W.WEECHAT_RC_OK - - if buffer == server.server_buffer: - message = ( - '{prefix}matrix: command "me" must be ' - "executed on a Matrix channel buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_topic_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.topic(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "topic" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - if not parsed_args.topic and not parsed_args.delete: - # TODO print the current topic - return W.WEECHAT_RC_OK - - if parsed_args.delete and parsed_args.topic: - # TODO error message - return W.WEECHAT_RC_OK - - topic = "" if parsed_args.delete else " ".join(parsed_args.topic) - content = {"topic": topic} - server.room_send_state(room, content, "m.room.topic") - - return W.WEECHAT_RC_OK - - -def matrix_fetch_old_messages(server, room_id): - room_buffer = server.find_room_from_id(room_id) - room = room_buffer.room - - if room_buffer.backlog_pending: - return - - prev_batch = room.prev_batch - - if not prev_batch: - return - - raise NotImplementedError - - -def check_server_existence(server_name, servers): - if server_name not in servers: - message = "{prefix}matrix: No such server: {server}".format( - prefix=W.prefix("error"), server=server_name - ) - W.prnt("", message) - return False - return True - - -def hook_page_up(): - G.CONFIG.page_up_hook = W.hook_command_run( - "/window page_up", "matrix_command_pgup_cb", "" - ) - - -@utf8_decode -def matrix_command_buf_clear_cb(data, buffer, command): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room_buffer.room.prev_batch = server.next_batch - - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_command_pgup_cb(data, buffer, command): - # TODO the highlight status of a line isn't allowed to be updated/changed - # via hdata, therefore the highlight status of a messages can't be - # reoredered this would need to be fixed in weechat - # TODO we shouldn't fetch and print out more messages than - # max_buffer_lines_number or older messages than max_buffer_lines_minutes - for server in SERVERS.values(): - if buffer in server.buffers.values(): - window = W.window_search_with_buffer(buffer) - - first_line_displayed = bool( - W.window_get_integer(window, "first_line_displayed") - ) - - room_buffer = server.find_room_from_ptr(buffer) - - if first_line_displayed or room_buffer.weechat_buffer.num_lines == 0: - room_id = key_from_value(server.buffers, buffer) - server.room_get_messages(room_id) - - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_join_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.join(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - server.room_join(parsed_args.room_id) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_part_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.part(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - room_id = parsed_args.room_id - - if not room_id: - if buffer == server.server_buffer: - server.error( - 'command "part" must be ' - "executed on a Matrix room buffer or a room " - "name needs to be given" - ) - return W.WEECHAT_RC_OK - - room_buffer = server.find_room_from_ptr(buffer) - room_id = room_buffer.room.room_id - - server.room_leave(room_id) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_invite_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.invite(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "invite" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - user_id = parsed_args.user_id - user_id = user_id if user_id.startswith("@") else "@" + user_id - - server.room_invite(room, user_id) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_room_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.room(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "room" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - if not parsed_args.subcommand or parsed_args.subcommand == "list": - server.error("command no subcommand found") - return W.WEECHAT_RC_OK - - if parsed_args.subcommand == "typing-notifications": - if parsed_args.state == "enable": - room.typing_enabled = True - elif parsed_args.state == "disable": - room.typing_enabled = False - elif parsed_args.state == "toggle": - room.typing_enabled = not room.typing_enabled - break - - elif parsed_args.subcommand == "read-markers": - if parsed_args.state == "enable": - room.read_markers_enabled = True - elif parsed_args.state == "disable": - room.read_markers_enabled = False - elif parsed_args.state == "toggle": - room.read_markers_enabled = not room.read_markers_enabled - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_uploads_command_cb(data, buffer, args): - if not args: - if not G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer = UploadsBuffer() - G.CONFIG.upload_buffer.display() - return W.WEECHAT_RC_OK - - parsed_args = WeechatCommandParser.uploads(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - if parsed_args.subcommand == "list": - pass - elif parsed_args.subcommand == "listfull": - pass - elif parsed_args.subcommand == "up": - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.move_line_up() - elif parsed_args.subcommand == "down": - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.move_line_down() - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_upload_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.upload(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "upload" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room_buffer = server.find_room_from_ptr(buffer) - if not room_buffer: - continue - - upload = Upload( - server.name, - server.config.address, - server.client.access_token, - room_buffer.room.room_id, - parsed_args.file, - room_buffer.room.encrypted - ) - UPLOADS[upload.uuid] = upload - - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.render() - - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_kick_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.kick(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "kick" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - user_id = parsed_args.user_id - user_id = user_id if user_id.startswith("@") else "@" + user_id - reason = " ".join(parsed_args.reason) if parsed_args.reason else None - - server.room_kick(room, user_id, reason) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_redact_command_cb(data, buffer, args): - def already_redacted(line): - if SCRIPT_NAME + "_redacted" in line.tags: - return True - return False - - def predicate(event_id, line): - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - tags = line.tags - - if event_tag in tags: - return True - - return False - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - - event_id, reason = parse_redact_args(args) - - if not event_id: - message = ( - "{prefix}matrix: Invalid command " - "arguments (see /help redact)" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_ERROR - - lines = room_buffer.weechat_buffer.find_lines( - partial(predicate, event_id), max_lines=1 - ) - - if not lines: - room_buffer.error( - "No such message with event id " - "{event_id} found.".format(event_id=event_id)) - return W.WEECHAT_RC_OK - - if already_redacted(lines[0]): - room_buffer.error("Event already redacted.") - return W.WEECHAT_RC_OK - - server.room_send_redaction(room_buffer, event_id, reason) - - return W.WEECHAT_RC_OK - - if buffer == server.server_buffer: - message = ( - '{prefix}matrix: command "redact" must be ' - "executed on a Matrix channel buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_reply_command_cb(data, buffer, args): - def predicate(event_id, line): - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - tags = line.tags - - if event_tag in tags: - return True - return False - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - - # Intentional use of `parse_redact_args` which serves the - # necessary purpose - event_id, reply = parse_redact_args(args) - - if not event_id or not reply: - message = ( - "{prefix}matrix: Invalid command " - "arguments (see /help reply)" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_ERROR - - lines = room_buffer.weechat_buffer.find_lines( - partial(predicate, event_id), max_lines=1 - ) - - if not lines: - room_buffer.error( - "No such message with event id " - "{event_id} found.".format(event_id=event_id)) - return W.WEECHAT_RC_OK - - formatted_data = Formatted.from_input_line(reply) - server.room_send_message( - room_buffer, - formatted_data, - "m.text", - in_reply_to_event_id=event_id, - ) - room_buffer.last_message = None - - return W.WEECHAT_RC_OK - - if buffer == server.server_buffer: - message = ( - '{prefix}matrix: command "reply" must be ' - "executed on a Matrix channel buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -def matrix_command_help(args): - if not args: - message = ( - "{prefix}matrix: Too few arguments for command " - '"/matrix help" (see /matrix help help)' - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return - - for command in args: - message = "" - - if command == "connect": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/connect{ncolor} " - " [...]" - "\n\n" - "connect to Matrix server(s)" - "\n\n" - "server-name: server to connect to" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "disconnect": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/disconnect{ncolor} " - " [...]" - "\n\n" - "disconnect from Matrix server(s)" - "\n\n" - "server-name: server to disconnect" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "reconnect": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/reconnect{ncolor} " - " [...]" - "\n\n" - "reconnect to Matrix server(s)" - "\n\n" - "server-name: server to reconnect" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "server": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/server{ncolor} " - "add [:]" - "\n " - "delete|list|listfull " - "\n\n" - "list, add, or remove Matrix servers" - "\n\n" - " list: list servers (without argument, this " - "list is displayed)\n" - " listfull: list servers with detailed info for each " - "server\n" - " add: add a new server\n" - " delete: delete a server\n" - "server-name: server to reconnect (internal name)\n" - " hostname: name or IP address of server\n" - " port: port of server (default: 443)\n" - "\n" - "Examples:" - "\n /matrix server listfull" - "\n /matrix server add matrix matrix.org:80" - "\n /matrix server delete matrix" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "help": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/help{ncolor} " - " [...]" - "\n\n" - "display help about Matrix commands" - "\n\n" - "matrix-command: a Matrix command name" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - else: - message = ( - '{prefix}matrix: No help available, "{command}" ' - "is not a matrix command" - ).format(prefix=W.prefix("error"), command=command) - - W.prnt("", "") - W.prnt("", message) - - return - - -def matrix_server_command_listfull(args): - def get_value_string(value, default_value): - if value == default_value: - if not value: - value = "''" - value_string = " ({value})".format(value=value) - else: - value_string = "{color}{value}{ncolor}".format( - color=W.color("chat_value"), - value=value, - ncolor=W.color("reset"), - ) - - return value_string - - for server_name in args: - if server_name not in SERVERS: - continue - - server = SERVERS[server_name] - connected = "" - - W.prnt("", "") - - if server.connected: - connected = "connected" - else: - connected = "not connected" - - message = ( - "Server: {server_color}{server}{delimiter_color}" - " [{ncolor}{connected}{delimiter_color}]" - "{ncolor}" - ).format( - server_color=W.color("chat_server"), - server=server.name, - delimiter_color=W.color("chat_delimiters"), - connected=connected, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - - option = server.config._option_ptrs["autoconnect"] - default_value = W.config_string_default(option) - value = W.config_string(option) - - value_string = get_value_string(value, default_value) - message = " autoconnect. : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["address"] - default_value = W.config_string_default(option) - value = W.config_string(option) - - value_string = get_value_string(value, default_value) - message = " address. . . : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["port"] - default_value = str(W.config_integer_default(option)) - value = str(W.config_integer(option)) - - value_string = get_value_string(value, default_value) - message = " port . . . . : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["username"] - default_value = W.config_string_default(option) - value = W.config_string(option) - - value_string = get_value_string(value, default_value) - message = " username . . : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["password"] - value = W.config_string(option) - - if value: - value = "(hidden)" - - value_string = get_value_string(value, "") - message = " password . . : {value}".format(value=value_string) - - W.prnt("", message) - - -def matrix_server_command_delete(args): - for server_name in args: - if check_server_existence(server_name, SERVERS): - server = SERVERS[server_name] - - if server.connected: - message = ( - "{prefix}matrix: you can not delete server " - "{color}{server}{ncolor} because you are " - 'connected to it. Try "/matrix disconnect ' - '{color}{server}{ncolor}" before.' - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - ncolor=W.color("reset"), - server=server.name, - ) - W.prnt("", message) - return - - for buf in list(server.buffers.values()): - W.buffer_close(buf) - - if server.server_buffer: - W.buffer_close(server.server_buffer) - - for option in server.config._option_ptrs.values(): - W.config_option_free(option) - - if server.timer_hook: - W.unhook(server.timer_hook) - server.timer_hook = None - - message = ( - "matrix: server {color}{server}{ncolor} has been " "deleted" - ).format( - server=server.name, - color=W.color("chat_server"), - ncolor=W.color("reset"), - ) - - del SERVERS[server.name] - server = None - - W.prnt("", message) - - -def matrix_server_command_add(args): - if len(args) < 2: - message = ( - "{prefix}matrix: Too few arguments for command " - '"/matrix server add" (see /matrix help server)' - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return - if len(args) > 4: - message = ( - "{prefix}matrix: Too many arguments for command " - '"/matrix server add" (see /matrix help server)' - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return - - def remove_server(server): - for option in server.config._option_ptrs.values(): - W.config_option_free(option) - del SERVERS[server.name] - - server_name = args[0] - - if server_name in SERVERS: - message = ( - "{prefix}matrix: server {color}{server}{ncolor} " - "already exists, can't add it" - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server_name, - ncolor=W.color("reset"), - ) - W.prnt("", message) - return - - server = MatrixServer(server_name, G.CONFIG._ptr) - SERVERS[server.name] = server - - if len(args) >= 2: - if args[1].startswith("http"): - homeserver= urlparse(args[1]) - - host = homeserver.hostname - port = str(homeserver.port) if homeserver.port else None - else: - try: - host, port = args[1].split(":", 1) - except ValueError: - host, port = args[1], None - - return_code = W.config_option_set( - server.config._option_ptrs["address"], host, 1 - ) - - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set address for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - server = None - return - - if port: - return_code = W.config_option_set( - server.config._option_ptrs["port"], port, 1 - ) - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set port for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - server = None - return - - if len(args) >= 3: - user = args[2] - return_code = W.config_option_set( - server.config._option_ptrs["username"], user, 1 - ) - - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set user for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - server = None - return - - if len(args) == 4: - password = args[3] - - return_code = W.config_option_set( - server.config._option_ptrs["password"], password, 1 - ) - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set password for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - W.prnt("", message) - server = None - return - - message = ( - "matrix: server {color}{server}{ncolor} " "has been added" - ).format( - server=server.name, - color=W.color("chat_server"), - ncolor=W.color("reset"), - ) - W.prnt("", message) - - -def matrix_server_command(command, args): - def list_servers(_): - if SERVERS: - W.prnt("", "\nAll matrix servers:") - for server in SERVERS: - W.prnt( - "", - " {color}{server}".format( - color=W.color("chat_server"), server=server - ), - ) - - # TODO the argument for list and listfull is used as a match word to - # find/filter servers, we're currently match exactly to the whole name - if command == "list": - list_servers(args) - elif command == "listfull": - matrix_server_command_listfull(args) - elif command == "add": - matrix_server_command_add(args) - elif command == "delete": - matrix_server_command_delete(args) - else: - message = ( - "{prefix}matrix: Error: unknown matrix server command, " - '"{command}" (type /matrix help server for help)' - ).format(prefix=W.prefix("error"), command=command) - W.prnt("", message) - - -@utf8_decode -def matrix_command_cb(data, buffer, args): - def connect_server(args): - for server_name in args: - if check_server_existence(server_name, SERVERS): - server = SERVERS[server_name] - server.connect() - - def disconnect_server(args): - for server_name in args: - if check_server_existence(server_name, SERVERS): - server = SERVERS[server_name] - if server.connected or server.reconnect_time: - # W.unhook(server.timer_hook) - # server.timer_hook = None - server.access_token = "" - server.disconnect(reconnect=False) - - split_args = list(filter(bool, args.split(" "))) - - if len(split_args) < 1: - message = ( - "{prefix}matrix: Too few arguments for command " - '"/matrix" ' - "(see /help matrix)" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_ERROR - - command, args = split_args[0], split_args[1:] - - if command == "connect": - connect_server(args) - - elif command == "disconnect": - disconnect_server(args) - - elif command == "reconnect": - disconnect_server(args) - connect_server(args) - - elif command == "server": - if len(args) >= 1: - subcommand, args = args[0], args[1:] - matrix_server_command(subcommand, args) - else: - matrix_server_command("list", "") - - elif command == "help": - matrix_command_help(args) - - else: - message = ( - "{prefix}matrix: Error: unknown matrix command, " - '"{command}" (type /help matrix for help)' - ).format(prefix=W.prefix("error"), command=command) - W.prnt("", message) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_send_anyways_cb(data, buffer, args): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - - if not server.connected: - room_buffer.error("Server is disconnected") - break - - if not server.client.logged_in: - room_buffer.error("You are not logged in.") - return W.WEECHAT_RC_ERROR - - if not room_buffer.last_message: - room_buffer.error("No previously sent message found.") - break - - server.room_send_message( - room_buffer, - room_buffer.last_message, - "m.text", - ignore_unverified_devices=True - ) - room_buffer.last_message = None - - break - else: - message = ( - "{prefix}matrix: The 'send-anyways' command needs to be " - "run on a matrix room buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - - return W.WEECHAT_RC_ERROR - - -@utf8_decode -def matrix_cursor_reply_signal_cb(data, signal, ht): - tags = ht["_chat_line_tags"].split(",") - - W.command("", "/cursor stop") - - if "matrix_message" in tags: - for tag in tags: - if tag.startswith("matrix_id_"): - matrix_id = tag[10:] - break - else: - return W.WEECHAT_RC_OK - - buffer_name = ht["_buffer_full_name"] - bufptr = W.buffer_search("==", buffer_name) - - current_input = W.buffer_get_string(bufptr, "input") - input_pos = W.buffer_get_integer(bufptr, "input_pos") - - new_prefix = "/reply-matrix {} ".format(matrix_id) - - W.buffer_set(bufptr, "input", new_prefix + current_input) - W.buffer_set(bufptr, "input_pos", str(len(new_prefix) + input_pos)) - - return W.WEECHAT_RC_OK diff --git a/weechat/python/matrix/completion.py b/weechat/python/matrix/completion.py deleted file mode 100644 index a1ac559..0000000 --- a/weechat/python/matrix/completion.py +++ /dev/null @@ -1,369 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -from typing import List, Optional -from matrix.globals import SERVERS, W, SCRIPT_NAME -from matrix.utf import utf8_decode -from matrix.utils import tags_from_line_data -from nio import LocalProtocolError - - -def add_servers_to_completion(completion): - for server_name in SERVERS: - W.hook_completion_list_add( - completion, server_name, 0, W.WEECHAT_LIST_POS_SORT - ) - - -@utf8_decode -def matrix_server_command_completion_cb( - data, completion_item, buffer, completion -): - buffer_input = W.buffer_get_string(buffer, "input").split() - - args = buffer_input[1:] - commands = ["add", "delete", "list", "listfull"] - - def complete_commands(): - for command in commands: - W.hook_completion_list_add( - completion, command, 0, W.WEECHAT_LIST_POS_SORT - ) - - if len(args) == 1: - complete_commands() - - elif len(args) == 2: - if args[1] not in commands: - complete_commands() - else: - if args[1] == "delete" or args[1] == "listfull": - add_servers_to_completion(completion) - - elif len(args) == 3: - if args[1] == "delete" or args[1] == "listfull": - if args[2] not in SERVERS: - add_servers_to_completion(completion) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_server_completion_cb(data, completion_item, buffer, completion): - add_servers_to_completion(completion) - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_command_completion_cb(data, completion_item, buffer, completion): - for command in [ - "connect", - "disconnect", - "reconnect", - "server", - "help", - "debug", - ]: - W.hook_completion_list_add( - completion, command, 0, W.WEECHAT_LIST_POS_SORT - ) - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_debug_completion_cb(data, completion_item, buffer, completion): - for debug_type in ["messaging", "network", "timing"]: - W.hook_completion_list_add( - completion, debug_type, 0, W.WEECHAT_LIST_POS_SORT - ) - return W.WEECHAT_RC_OK - - -# TODO this should be configurable -REDACTION_COMP_LEN = 50 - - -@utf8_decode -def matrix_message_completion_cb(data, completion_item, buffer, completion): - max_events = 500 - - def redacted_or_not_message(tags): - # type: (List[str]) -> bool - if SCRIPT_NAME + "_redacted" in tags: - return True - if SCRIPT_NAME + "_message" not in tags: - return True - - return False - - def event_id_from_tags(tags): - # type: (List[str]) -> Optional[str] - for tag in tags: - if tag.startswith("matrix_id"): - event_id = tag[10:] - return event_id - - return None - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - lines = room_buffer.weechat_buffer.lines - - added = 0 - - for line in lines: - tags = line.tags - if redacted_or_not_message(tags): - continue - - event_id = event_id_from_tags(tags) - - if not event_id: - continue - - # Make sure we'll be able to reliably detect the end of the - # quoted snippet - message_fmt = line.message.replace("\\", "\\\\") \ - .replace('"', '\\"') - - if len(message_fmt) > REDACTION_COMP_LEN + 2: - message_fmt = message_fmt[:REDACTION_COMP_LEN] + ".." - - item = ('{event_id}|"{message}"').format( - event_id=event_id, message=message_fmt - ) - - W.hook_completion_list_add( - completion, item, 0, W.WEECHAT_LIST_POS_END - ) - added += 1 - - if added >= max_events: - break - - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -def server_from_buffer(buffer): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - return server - if buffer == server.server_buffer: - return server - return None - - -@utf8_decode -def matrix_olm_user_completion_cb(data, completion_item, buffer, completion): - server = server_from_buffer(buffer) - - if not server: - return W.WEECHAT_RC_OK - - try: - device_store = server.client.device_store - except LocalProtocolError: - return W.WEECHAT_RC_OK - - for user in device_store.users: - W.hook_completion_list_add( - completion, user, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_olm_device_completion_cb(data, completion_item, buffer, completion): - server = server_from_buffer(buffer) - - if not server: - return W.WEECHAT_RC_OK - - try: - device_store = server.client.device_store - except LocalProtocolError: - return W.WEECHAT_RC_OK - - args = W.hook_completion_get_string(completion, "args") - - fields = args.split() - - if len(fields) < 2: - return W.WEECHAT_RC_OK - - user = fields[-1] - - if user not in device_store.users: - return W.WEECHAT_RC_OK - - for device in device_store.active_user_devices(user): - W.hook_completion_list_add( - completion, device.id, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_own_devices_completion_cb( - data, - completion_item, - buffer, - completion -): - server = server_from_buffer(buffer) - - if not server: - return W.WEECHAT_RC_OK - - olm = server.client.olm - - if not olm: - return W.WEECHAT_RC_OK - - W.hook_completion_list_add( - completion, olm.device_id, 0, W.WEECHAT_LIST_POS_SORT - ) - - user = olm.user_id - - if user not in olm.device_store.users: - return W.WEECHAT_RC_OK - - for device in olm.device_store.active_user_devices(user): - W.hook_completion_list_add( - completion, device.id, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_user_completion_cb(data, completion_item, buffer, completion): - def add_user(completion, user): - W.hook_completion_list_add( - completion, user, 0, W.WEECHAT_LIST_POS_SORT - ) - - for server in SERVERS.values(): - if buffer == server.server_buffer: - return W.WEECHAT_RC_OK - - room_buffer = server.find_room_from_ptr(buffer) - - if not room_buffer: - continue - - users = room_buffer.room.users - - users = [user[1:] for user in users] - - for user in users: - add_user(completion, user) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_room_completion_cb(data, completion_item, buffer, completion): - """Completion callback for matrix room names.""" - for server in SERVERS.values(): - for room_buffer in server.room_buffers.values(): - name = room_buffer.weechat_buffer.short_name - - W.hook_completion_list_add( - completion, name, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -def init_completion(): - W.hook_completion( - "matrix_server_commands", - "Matrix server completion", - "matrix_server_command_completion_cb", - "", - ) - - W.hook_completion( - "matrix_servers", - "Matrix server completion", - "matrix_server_completion_cb", - "", - ) - - W.hook_completion( - "matrix_commands", - "Matrix command completion", - "matrix_command_completion_cb", - "", - ) - - W.hook_completion( - "matrix_messages", - "Matrix message completion", - "matrix_message_completion_cb", - "", - ) - - W.hook_completion( - "matrix_debug_types", - "Matrix debugging type completion", - "matrix_debug_completion_cb", - "", - ) - - W.hook_completion( - "olm_user_ids", - "Matrix olm user id completion", - "matrix_olm_user_completion_cb", - "", - ) - - W.hook_completion( - "olm_devices", - "Matrix olm device id completion", - "matrix_olm_device_completion_cb", - "", - ) - - W.hook_completion( - "matrix_users", - "Matrix user id completion", - "matrix_user_completion_cb", - "", - ) - - W.hook_completion( - "matrix_own_devices", - "Matrix own devices completion", - "matrix_own_devices_completion_cb", - "", - ) - - W.hook_completion( - "matrix_rooms", - "Matrix room name completion", - "matrix_room_completion_cb", - "", - ) diff --git a/weechat/python/matrix/config.py b/weechat/python/matrix/config.py deleted file mode 100644 index f4ff40a..0000000 --- a/weechat/python/matrix/config.py +++ /dev/null @@ -1,916 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -"""weechat-matrix Configuration module. - -This module contains abstractions on top of weechats configuration files and -the main script configuration class. - -To add configuration options refer to MatrixConfig. -Server specific configuration options are handled in server.py -""" - -from builtins import super -from collections import namedtuple -from enum import IntEnum, Enum, unique - -import logbook - -import nio -from matrix.globals import SCRIPT_NAME, SERVERS, W -from matrix.utf import utf8_decode - -from . import globals as G - - -@unique -class RedactType(Enum): - STRIKETHROUGH = 0 - NOTICE = 1 - DELETE = 2 - - -@unique -class ServerBufferType(Enum): - MERGE_CORE = 0 - MERGE = 1 - INDEPENDENT = 2 - - -@unique -class NewChannelPosition(IntEnum): - NONE = 0 - NEXT = 1 - NEAR_SERVER = 2 - - -nio.logger_group.level = logbook.ERROR - - -class Option( - namedtuple( - "Option", - [ - "name", - "type", - "string_values", - "min", - "max", - "value", - "description", - "cast_func", - "change_callback", - ], - ) -): - """A class representing a new configuration option. - - An option object is consumed by the ConfigSection class adding - configuration options to weechat. - """ - - __slots__ = () - - def __new__( - cls, - name, - type, - string_values, - min, - max, - value, - description, - cast=None, - change_callback=None, - ): - """ - Parameters: - name (str): Name of the configuration option - type (str): Type of the configuration option, can be one of the - supported weechat types: string, boolean, integer, color - string_values: (str): A list of string values that the option can - accept seprated by | - min (int): Minimal value of the option, only used if the type of - the option is integer - max (int): Maximal value of the option, only used if the type of - the option is integer - description (str): Description of the configuration option - cast (callable): A callable function taking a single value and - returning a modified value. Useful to turn the configuration - option into an enum while reading it. - change_callback(callable): A function that will be called - by weechat every time the configuration option is changed. - """ - - return super().__new__( - cls, - name, - type, - string_values, - min, - max, - value, - description, - cast, - change_callback, - ) - - -@utf8_decode -def matrix_config_reload_cb(data, config_file): - return W.WEECHAT_RC_OK - - -def change_log_level(category, level): - """Change the log level of the underlying nio lib - - Called every time the user changes the log level or log category - configuration option.""" - - if category == "all": - nio.logger_group.level = level - elif category == "http": - nio.http.logger.level = level - elif category == "client": - nio.client.logger.level = level - elif category == "events": - nio.events.logger.level = level - elif category == "responses": - nio.responses.logger.level = level - elif category == "encryption": - nio.crypto.logger.level = level - - -@utf8_decode -def config_server_buffer_cb(data, option): - """Callback for the look.server_buffer option. - Is called when the option is changed and merges/splits the server - buffer""" - - for server in SERVERS.values(): - server.buffer_merge() - return 1 - - -@utf8_decode -def config_log_level_cb(data, option): - """Callback for the network.debug_level option.""" - change_log_level( - G.CONFIG.network.debug_category, G.CONFIG.network.debug_level - ) - return 1 - - -@utf8_decode -def config_log_category_cb(data, option): - """Callback for the network.debug_category option.""" - change_log_level(G.CONFIG.debug_category, logbook.ERROR) - G.CONFIG.debug_category = G.CONFIG.network.debug_category - change_log_level( - G.CONFIG.network.debug_category, G.CONFIG.network.debug_level - ) - return 1 - - -@utf8_decode -def config_pgup_cb(data, option): - """Callback for the network.fetch_backlog_on_pgup option. - Enables or disables the hook that is run when /window page_up is called""" - if G.CONFIG.network.fetch_backlog_on_pgup: - if not G.CONFIG.page_up_hook: - G.CONFIG.page_up_hook = W.hook_command_run( - "/window page_up", "matrix_command_pgup_cb", "" - ) - else: - if G.CONFIG.page_up_hook: - W.unhook(G.CONFIG.page_up_hook) - G.CONFIG.page_up_hook = None - - return 1 - - -def level_to_logbook(value): - if value == 0: - return logbook.ERROR - if value == 1: - return logbook.WARNING - if value == 2: - return logbook.INFO - if value == 3: - return logbook.DEBUG - - return logbook.ERROR - - -def logbook_category(value): - if value == 0: - return "all" - if value == 1: - return "http" - if value == 2: - return "client" - if value == 3: - return "events" - if value == 4: - return "responses" - if value == 5: - return "encryption" - - return "all" - - -def parse_nick_prefix_colors(value): - """Parses the nick prefix color setting string - ("admin=COLOR1;mod=COLOR2;power=COLOR3") into a prefix -> color dict.""" - - def key_to_prefix(key): - if key == "admin": - return "&" - elif key == "mod": - return "@" - elif key == "power": - return "+" - else: - return "" - - prefix_colors = { - "&": "lightgreen", - "@": "lightgreen", - "+": "yellow", - } - - for setting in value.split(";"): - # skip malformed settings - if "=" not in setting: - continue - - key, color = setting.split("=") - prefix = key_to_prefix(key) - - if prefix: - prefix_colors[prefix] = color - - return prefix_colors - - -def eval_cast(string): - """A function that passes a string to weechat which evaluates it using its - expression evaluation syntax. - Can only be used with strings, useful for passwords or options that contain - a formatted string to e.g. add colors. - More info here: - https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_string_eval_expression""" - - return W.string_eval_expression(string, {}, {}, {}) - - -class WeechatConfig(object): - """A class representing a weechat configuration file - Wraps weechats configuration creation functionality""" - - def __init__(self, sections): - """Create a new weechat configuration file, expects the global - SCRIPT_NAME to be defined and a reload callback - - Parameters: - sections (List[Tuple[str, List[Option]]]): List of config sections - that will be created for the configuration file. - """ - self._ptr = W.config_new( - SCRIPT_NAME, SCRIPT_NAME + "_config_reload_cb", "" - ) - - for section in sections: - name, options = section - section_class = ConfigSection.build(name, options) - setattr(self, name, section_class(name, self._ptr, options)) - - def free(self): - """Free all the config sections and their options as well as the - configuration file. Should be called when the script is unloaded.""" - for section in [ - getattr(self, a) - for a in dir(self) - if isinstance(getattr(self, a), ConfigSection) - ]: - section.free() - - W.config_free(self._ptr) - - def read(self): - """Read the config file""" - return_code = W.config_read(self._ptr) - if return_code == W.WEECHAT_CONFIG_READ_OK: - return True - if return_code == W.WEECHAT_CONFIG_READ_MEMORY_ERROR: - return False - if return_code == W.WEECHAT_CONFIG_READ_FILE_NOT_FOUND: - return True - return False - - -class ConfigSection(object): - """A class representing a weechat config section. - Should not be used on its own, the WeechatConfig class uses this to build - config sections.""" - @classmethod - def build(cls, name, options): - def constructor(self, name, config_ptr, options): - self._ptr = W.config_new_section( - config_ptr, name, 0, 0, "", "", "", "", "", "", "", "", "", "" - ) - self._config_ptr = config_ptr - self._option_ptrs = {} - - for option in options: - self._add_option(option) - - attributes = { - option.name: cls.option_property( - option.name, option.type, cast_func=option.cast_func - ) - for option in options - } - attributes["__init__"] = constructor - - section_class = type(name.title() + "Section", (cls,), attributes) - return section_class - - def free(self): - W.config_section_free_options(self._ptr) - W.config_section_free(self._ptr) - - def _add_option(self, option): - cb = option.change_callback.__name__ if option.change_callback else "" - option_ptr = W.config_new_option( - self._config_ptr, - self._ptr, - option.name, - option.type, - option.description, - option.string_values, - option.min, - option.max, - option.value, - option.value, - 0, - "", - "", - cb, - "", - "", - "", - ) - - self._option_ptrs[option.name] = option_ptr - - @staticmethod - def option_property(name, option_type, evaluate=False, cast_func=None): - """Create a property for this class that makes the reading of config - option values pythonic. The option will be available as a property with - the name of the option. - If a cast function was defined for the option the property will pass - the option value to the cast function and return its result.""" - - def bool_getter(self): - return bool(W.config_boolean(self._option_ptrs[name])) - - def str_getter(self): - if cast_func: - return cast_func(W.config_string(self._option_ptrs[name])) - return W.config_string(self._option_ptrs[name]) - - def str_evaluate_getter(self): - return W.string_eval_expression( - W.config_string(self._option_ptrs[name]), {}, {}, {} - ) - - def int_getter(self): - if cast_func: - return cast_func(W.config_integer(self._option_ptrs[name])) - return W.config_integer(self._option_ptrs[name]) - - if option_type in ("string", "color"): - if evaluate: - return property(str_evaluate_getter) - return property(str_getter) - if option_type == "boolean": - return property(bool_getter) - if option_type == "integer": - return property(int_getter) - - -class MatrixConfig(WeechatConfig): - """Main matrix configuration file. - This class defines all the global matrix configuration options. - New global options should be added to the constructor of this class under - the appropriate section. - - There are three main sections defined: - Look: This section is for options that change the way matrix messages - are shown or the way the buffers are shown. - Color: This section should mainly be for color options, options that - change color schemes or themes should go to the look section. - Network: This section is for options that change the way the script - behaves, e.g. the way it communicates with the server, it handles - responses or any other behavioural change that doesn't fit in the - previous sections. - - There is a special section called server defined which contains per server - configuration options. Server options aren't defined here, they need to be - added in server.py - """ - - def __init__(self): - self.debug_buffer = "" - self.upload_buffer = "" - self.debug_category = "all" - self.page_up_hook = None - self.human_buffer_names = None - - look_options = [ - Option( - "redactions", - "integer", - "strikethrough|notice|delete", - 0, - 0, - "strikethrough", - ( - "Only notice redactions, strike through or delete " - "redacted messages" - ), - RedactType, - ), - Option( - "server_buffer", - "integer", - "merge_with_core|merge_without_core|independent", - 0, - 0, - "merge_with_core", - "Merge server buffers", - ServerBufferType, - config_server_buffer_cb, - ), - Option( - "new_channel_position", - "integer", - "none|next|near_server", - min(NewChannelPosition), - max(NewChannelPosition), - "none", - "force position of new channel in list of buffers " - "(none = default position (should be last buffer), " - "next = current buffer + 1, near_server = after last " - "channel/pv of server)", - NewChannelPosition, - ), - Option( - "max_typing_notice_item_length", - "integer", - "", - 10, - 1000, - "50", - ("Limit the length of the typing notice bar item."), - ), - Option( - "bar_item_typing_notice_prefix", - "string", - "", - 0, - 0, - "Typing: ", - ("Prefix for the typing notice bar item."), - ), - Option( - "encryption_warning_sign", - "string", - "", - 0, - 0, - "⚠️ ", - ("A sign that is used to signal trust issues in encrypted " - "rooms (note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "busy_sign", - "string", - "", - 0, - 0, - "⏳", - ("A sign that is used to signal that the client is busy e.g. " - "when the room backlog is fetching" - " (note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "encrypted_room_sign", - "string", - "", - 0, - 0, - "🔐", - ("A sign that is used to show that the current room is " - "encrypted " - "(note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "disconnect_sign", - "string", - "", - 0, - 0, - "❌", - ("A sign that is used to show that the server is disconnected " - "(note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "pygments_style", - "string", - "", - 0, - 0, - "native", - "Pygments style to use for highlighting source code blocks", - ), - Option( - "code_blocks", - "boolean", - "", - 0, - 0, - "on", - ("Display preformatted code blocks as rectangular areas by " - "padding them with whitespace up to the length of the longest" - " line (with optional margin)"), - ), - Option( - "code_block_margin", - "integer", - "", - 0, - 100, - "2", - ("Number of spaces to add as a margin around around a code " - "block"), - ), - Option( - "quote_wrap", - "integer", - "", - -1, - 1000, - "67", - ("After how many characters to soft-wrap lines in a quote " - "block (reply message). Set to -1 to disable soft-wrapping."), - ), - Option( - "human_buffer_names", - "boolean", - "", - 0, - 0, - "off", - ("If turned on the buffer name will consist of the server " - "name and the room name instead of the Matrix room ID. Note, " - "this requires a change to the logger.file.mask setting " - "since conflicts can happen otherwise " - "(requires a script reload)."), - ), - Option( - "markdown_input", - "boolean", - "", - 0, - 0, - "on", - ("If turned on, markdown usage in messages will be converted " - "to actual markup (**bold**, *italic*, _italic_, `code`)."), - ), - ] - - network_options = [ - Option( - "max_initial_sync_events", - "integer", - "", - 1, - 10000, - "30", - ("How many events to fetch during the initial sync"), - ), - Option( - "max_backlog_sync_events", - "integer", - "", - 1, - 100, - "10", - ("How many events to fetch during backlog fetching"), - ), - Option( - "fetch_backlog_on_pgup", - "boolean", - "", - 0, - 0, - "on", - ("Fetch messages in the backlog on a window page up event"), - None, - config_pgup_cb, - ), - Option( - "debug_level", - "integer", - "error|warn|info|debug", - 0, - 0, - "error", - "Enable network protocol debugging.", - level_to_logbook, - config_log_level_cb, - ), - Option( - "debug_category", - "integer", - "all|http|client|events|responses|encryption", - 0, - 0, - "all", - "Debugging category", - logbook_category, - config_log_category_cb, - ), - Option( - "debug_buffer", - "boolean", - "", - 0, - 0, - "off", - ("Use a separate buffer for debug logs."), - ), - Option( - "lazy_load_room_users", - "boolean", - "", - 0, - 0, - "off", - ("If on, room users won't be loaded in the background " - "proactively, they will be loaded when the user switches to " - "the room buffer. This only affects non-encrypted rooms."), - ), - Option( - "max_nicklist_users", - "integer", - "", - 100, - 20000, - "5000", - ("Limit the number of users that are added to the nicklist. " - "Active users and users with a higher power level are always." - " Inactive users will be removed from the nicklist after a " - "day of inactivity."), - ), - Option( - "lag_reconnect", - "integer", - "", - 5, - 604800, - "90", - ("Reconnect to the server if the lag is greater than this " - "value (in seconds)"), - ), - Option( - "autoreconnect_delay_growing", - "integer", - "", - 1, - 100, - "2", - ("growing factor for autoreconnect delay to server " - "(1 = always same delay, 2 = delay*2 for each retry, etc.)"), - ), - Option( - "autoreconnect_delay_max", - "integer", - "", - 0, - 604800, - "600", - ("maximum autoreconnect delay to server " - "(in seconds, 0 = no maximum)"), - ), - Option( - "print_unconfirmed_messages", - "boolean", - "", - 0, - 0, - "on", - ("If off, messages are only printed after the server confirms " - "their receival. If on, messages are immediately printed but " - "colored differently until receival is confirmed."), - ), - Option( - "lag_min_show", - "integer", - "", - 1, - 604800, - "500", - ("minimum lag to show (in milliseconds)"), - ), - Option( - "typing_notice_conditions", - "string", - "", - 0, - 0, - "${typing_enabled}", - ("conditions to send typing notifications (note: content is " - "evaluated, see /help eval); besides the buffer and window " - "variables the typing_enabled variable is also expanded; " - "the typing_enabled variable can be manipulated with the " - "/room command, see /help room"), - ), - Option( - "read_markers_conditions", - "string", - "", - 0, - 0, - "${markers_enabled}", - ("conditions to send read markers (note: content is " - "evaluated, see /help eval); besides the buffer and window " - "variables the markers_enabled variable is also expanded; " - "the markers_enabled variable can be manipulated with the " - "/room command, see /help room"), - ), - Option( - "resending_ignores_devices", - "boolean", - "", - 0, - 0, - "on", - ("If on resending the same message to a room that contains " - "unverified devices will mark the devices as ignored and " - "continue sending the message. If off resending the message " - "will again fail and devices need to be marked as verified " - "one by one or the /send-anyways command needs to be used to " - "ignore them."), - ), - ] - - color_options = [ - Option( - "quote_fg", - "color", - "", - 0, - 0, - "lightgreen", - "Foreground color for matrix style blockquotes", - ), - Option( - "quote_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of quote_fg", - ), - Option( - "error_message_fg", - "color", - "", - 0, - 0, - "darkgray", - ("Foreground color for error messages that appear inside a " - "room buffer (e.g. when a message errors out when sending or " - "when a message is redacted)"), - ), - Option( - "error_message_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of error_message_fg.", - ), - Option( - "unconfirmed_message_fg", - "color", - "", - 0, - 0, - "darkgray", - ("Foreground color for messages that are printed out but the " - "server hasn't confirmed the that he received them."), - ), - Option( - "unconfirmed_message_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of unconfirmed_message_fg." - ), - Option( - "untagged_code_fg", - "color", - "", - 0, - 0, - "blue", - ("Foreground color for code without a language specifier. " - "Also used for `inline code`."), - ), - Option( - "untagged_code_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of untagged_code_fg", - ), - Option( - "nick_prefixes", - "string", - "", - 0, - 0, - "admin=lightgreen;mod=lightgreen;power=yellow", - ('Colors for nick prefixes indicating power level. ' - 'Format is "admin:color1;mod:color2;power:color3", ' - 'where "admin" stands for admins (power level = 100), ' - '"mod" stands for moderators (power level >= 50) and ' - '"power" for any other power user (power level > 0). ' - 'Requires restart to apply changes.'), - parse_nick_prefix_colors, - ), - ] - - sections = [ - ("network", network_options), - ("look", look_options), - ("color", color_options), - ] - - super().__init__(sections) - - # The server section is essentially a section with subsections and no - # options, handle that case independently. - W.config_new_section( - self._ptr, - "server", - 0, - 0, - "matrix_config_server_read_cb", - "", - "matrix_config_server_write_cb", - "", - "", - "", - "", - "", - "", - "", - ) - - def read(self): - super().read() - self.human_buffer_names = self.look.human_buffer_names - - def free(self): - section_ptr = W.config_search_section(self._ptr, "server") - W.config_section_free(section_ptr) - super().free() diff --git a/weechat/python/matrix/globals.py b/weechat/python/matrix/globals.py deleted file mode 100644 index c3e099e..0000000 --- a/weechat/python/matrix/globals.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import sys -from typing import Any, Dict, Optional -from logbook import Logger -from collections import OrderedDict - -from .utf import WeechatWrapper - -if False: - from .server import MatrixServer - from .config import MatrixConfig - from .uploads import Upload - - -try: - import weechat - - W = weechat if sys.hexversion >= 0x3000000 else WeechatWrapper(weechat) -except ImportError: - import matrix._weechat as weechat # type: ignore - - W = weechat - -SERVERS = dict() # type: Dict[str, MatrixServer] -CONFIG = None # type: Any -ENCRYPTION = True # type: bool -SCRIPT_NAME = "matrix" # type: str -BUFFER_NAME_PREFIX = "{}.".format(SCRIPT_NAME) # type: str -TYPING_NOTICE_TIMEOUT = 4000 # 4 seconds typing notice lifetime -LOGGER = Logger("weechat-matrix") -UPLOADS = OrderedDict() # type: Dict[str, Upload] diff --git a/weechat/python/matrix/message_renderer.py b/weechat/python/matrix/message_renderer.py deleted file mode 100644 index baa1fda..0000000 --- a/weechat/python/matrix/message_renderer.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -"""Module for rendering matrix messages in Weechat.""" - -from __future__ import unicode_literals -from nio import Api -from .globals import W -from .colors import Formatted - - -class Render(object): - """Class collecting methods for rendering matrix messages in Weechat.""" - - @staticmethod - def _media(url, description): - return ("{del_color}<{ncolor}{desc}{del_color}>{ncolor} " - "{del_color}[{ncolor}{url}{del_color}]{ncolor}").format( - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - desc=description, url=url) - - @staticmethod - def media(mxc, body, homeserver=None): - """Render a mxc media URI.""" - url = Api.mxc_to_http(mxc, homeserver) - description = "{}".format(body) if body else "file" - return Render._media(url, description) - - @staticmethod - def encrypted_media(mxc, body, key, hash, iv, homeserver=None): - """Render a mxc media URI of an encrypted file.""" - http_url = Api.encrypted_mxc_to_plumb( - mxc, - key, - hash, - iv, - homeserver - ) - url = http_url if http_url else mxc - description = "{}".format(body) if body else "file" - return Render._media(url, description) - - @staticmethod - def message(body, formatted_body): - """Render a room message.""" - if formatted_body: - formatted = Formatted.from_html(formatted_body) - return formatted.to_weechat() - - return body - - @staticmethod - def redacted(censor, reason=None): - """Render a redacted event message.""" - reason = ( - ', reason: "{reason}"'.format(reason=reason) - if reason - else "" - ) - - data = ( - "{del_color}<{log_color}Message redacted by: " - "{censor}{log_color}{reason}{del_color}>{ncolor}" - ).format( - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - log_color=W.color("logger.color.backlog_line"), - censor=censor, - reason=reason, - ) - - return data - - @staticmethod - def room_encryption(nick): - """Render a room encryption event.""" - return "{nick} has enabled encryption in this room".format(nick=nick) - - @staticmethod - def unknown(message_type, content=None): - """Render a message of an unknown type.""" - content = ( - ': "{content}"'.format(content=content) - if content - else "" - ) - return "Unknown message of type {t}{c}".format( - t=message_type, - c=content - ) - - @staticmethod - def megolm(): - """Render an undecrypted megolm event.""" - return ("{del_color}<{log_color}Unable to decrypt: " - "The sender's device has not sent us " - "the keys for this message{del_color}>{ncolor}").format( - del_color=W.color("chat_delimiters"), - log_color=W.color("logger.color.backlog_line"), - ncolor=W.color("reset")) - - @staticmethod - def bad(event): - """Render a malformed event of a known type""" - return "Bad event received, event type: {t}".format(t=event.type) diff --git a/weechat/python/matrix/server.py b/weechat/python/matrix/server.py deleted file mode 100644 index dda861e..0000000 --- a/weechat/python/matrix/server.py +++ /dev/null @@ -1,2011 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import os -import pprint -import socket -import ssl -import time -import copy -from collections import defaultdict, deque -from atomicwrites import atomic_write -from typing import ( - Any, - Deque, - Dict, - Optional, - List, - NamedTuple, - DefaultDict, - Type, - Union, -) - -from uuid import UUID - -from nio import ( - Api, - HttpClient, - ClientConfig, - LocalProtocolError, - LoginResponse, - LoginInfoResponse, - Response, - Rooms, - RoomSendResponse, - RoomSendError, - SyncResponse, - ShareGroupSessionResponse, - ShareGroupSessionError, - KeysQueryResponse, - KeysClaimResponse, - DevicesResponse, - UpdateDeviceResponse, - DeleteDevicesAuthResponse, - DeleteDevicesResponse, - TransportType, - RoomMessagesResponse, - RoomMessagesError, - EncryptionError, - GroupEncryptionError, - OlmTrustError, - ErrorResponse, - SyncError, - LoginError, - JoinedMembersResponse, - JoinedMembersError, - RoomKeyEvent, - KeyVerificationStart, - KeyVerificationCancel, - KeyVerificationKey, - KeyVerificationMac, - KeyVerificationEvent, - ToDeviceMessage, - ToDeviceResponse, - ToDeviceError -) - -from . import globals as G -from .buffer import OwnAction, OwnMessage, RoomBuffer -from .config import ConfigSection, Option, ServerBufferType -from .globals import SCRIPT_NAME, SERVERS, W, TYPING_NOTICE_TIMEOUT -from .utf import utf8_decode -from .utils import create_server_buffer, key_from_value, server_buffer_prnt -from .uploads import Upload - -from .colors import Formatted, FormattedString, DEFAULT_ATTRIBUTES - -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse # type: ignore - -try: - FileNotFoundError # type: ignore -except NameError: - FileNotFoundError = IOError - - -EncryptionQueueItem = NamedTuple( - "EncryptionQueueItem", - [ - ("message_type", str), - ("message", Union[Formatted, Upload]), - ], -) - - -class ServerConfig(ConfigSection): - def __init__(self, server_name, config_ptr): - # type: (str, str) -> None - self._server_name = server_name - self._config_ptr = config_ptr - self._option_ptrs = {} # type: Dict[str, str] - - options = [ - Option( - "autoconnect", - "boolean", - "", - 0, - 0, - "off", - ( - "automatically connect to the matrix server when weechat " - "is starting" - ), - ), - Option( - "address", - "string", - "", - 0, - 0, - "", - ( - "Hostname or address of the server (note: content is " - "evaluated, see /help eval)" - ) - ), - Option( - "port", "integer", "", 0, 65535, "443", "Port for the server" - ), - Option( - "proxy", - "string", - "", - 0, - 0, - "", - ("Name of weechat proxy to use (see /help proxy) (note: " - "content is evaluated, see /help eval)"), - ), - Option( - "ssl_verify", - "boolean", - "", - 0, - 0, - "on", - ("Check that the SSL connection is fully trusted"), - ), - Option( - "username", - "string", - "", - 0, - 0, - "", - ( - "Username to use on the server (note: content is " - "evaluated, see /help eval)" - ) - ), - Option( - "password", - "string", - "", - 0, - 0, - "", - ( - "Password for the server (note: content is evaluated, see " - "/help eval)" - ), - ), - Option( - "device_name", - "string", - "", - 0, - 0, - "Weechat Matrix", - ( - "Device name to use when logging in, this " - "is only used on the firt login. Afther that the /devices " - "command can be used to change the device name. (note: " - "content is evaluated, see /help eval)" - ) - ), - Option( - "autoreconnect_delay", - "integer", - "", - 0, - 86400, - "10", - ("Delay (in seconds) before trying to reconnect to server"), - ), - Option( - "sso_helper_listening_port", - "integer", - "", - 0, - 65535, - "0", - ("The port that the SSO helpers web server should listen on"), - ), - ] - - section = W.config_search_section(config_ptr, "server") - self._ptr = section - - for option in options: - option_name = "{server}.{option}".format( - server=self._server_name, option=option.name - ) - - self._option_ptrs[option.name] = W.config_new_option( - config_ptr, - section, - option_name, - option.type, - option.description, - option.string_values, - option.min, - option.max, - option.value, - option.value, - 0, - "", - "", - "matrix_config_server_change_cb", - self._server_name, - "", - "", - ) - - autoconnect = ConfigSection.option_property("autoconnect", "boolean") - address = ConfigSection.option_property("address", "string", evaluate=True) - port = ConfigSection.option_property("port", "integer") - proxy = ConfigSection.option_property("proxy", "string", evaluate=True) - ssl_verify = ConfigSection.option_property("ssl_verify", "boolean") - username = ConfigSection.option_property("username", "string", - evaluate=True) - device_name = ConfigSection.option_property("device_name", "string", - evaluate=True) - reconnect_delay = ConfigSection.option_property("autoreconnect_delay", "integer") - password = ConfigSection.option_property( - "password", "string", evaluate=True - ) - sso_helper_listening_port = ConfigSection.option_property( - "sso_helper_listening_port", - "integer" - ) - - def free(self): - W.config_section_free_options(self._ptr) - - -class MatrixServer(object): - # pylint: disable=too-many-instance-attributes - def __init__(self, name, config_ptr): - # type: (str, str) -> None - # yapf: disable - self.name = name # type: str - self.user_id = "" - self.device_id = "" # type: str - - self.room_buffers = dict() # type: Dict[str, RoomBuffer] - self.buffers = dict() # type: Dict[str, str] - self.server_buffer = None # type: Optional[str] - self.fd_hook = None # type: Optional[str] - self.ssl_hook = None # type: Optional[str] - self.timer_hook = None # type: Optional[str] - self.numeric_address = "" # type: Optional[str] - - self._connected = False # type: bool - self.connecting = False # type: bool - self.reconnect_delay = 0 # type: int - self.reconnect_time = None # type: Optional[float] - self.sync_time = None # type: Optional[float] - self.socket = None # type: Optional[ssl.SSLSocket] - self.ssl_context = ssl.create_default_context() # type: ssl.SSLContext - self.transport_type = None # type: Optional[TransportType] - - self.sso_hook = None - - # Enable http2 negotiation on the ssl context. - self.ssl_context.set_alpn_protocols(["h2", "http/1.1"]) - - try: - self.ssl_context.set_npn_protocols(["h2", "http/1.1"]) - except NotImplementedError: - pass - - self.address = None - self.homeserver = None - self.client = None # type: Optional[HttpClient] - self.access_token = None # type: Optional[str] - self.next_batch = None # type: Optional[str] - self.transaction_id = 0 # type: int - self.lag = 0 # type: int - self.lag_done = False # type: bool - self.busy = False # type: bool - self.first_sync = True - - self.send_fd_hook = None # type: Optional[str] - self.send_buffer = b"" # type: bytes - self.device_check_timestamp = None # type: Optional[int] - - self.device_deletion_queue = dict() # type: Dict[str, str] - - self.encryption_queue = defaultdict(deque) \ - # type: DefaultDict[str, Deque[EncryptionQueueItem]] - self.backlog_queue = dict() # type: Dict[str, str] - - self.user_gc_time = time.time() # type: float - self.member_request_list = [] # type: List[str] - self.rooms_with_missing_members = [] # type: List[str] - self.lazy_load_hook = None # type: Optional[str] - - # These flags remember if we made some requests so that we don't - # make them again while we wait on a response, the flags need to be - # cleared when we disconnect. - self.keys_queried = False # type: bool - self.keys_claimed = defaultdict(bool) # type: Dict[str, bool] - self.group_session_shared = defaultdict(bool) # type: Dict[str, bool] - self.ignore_while_sharing = defaultdict(bool) # type: Dict[str, bool] - self.to_device_sent = [] # type: List[ToDeviceMessage] - - # Try to load the device id, the device id is loaded every time the - # user changes but some login flows don't use a user so try to load the - # device for a main user. - self._load_device_id("main") - self.config = ServerConfig(self.name, config_ptr) - self._create_session_dir() - # yapf: enable - - def _create_session_dir(self): - path = os.path.join("matrix", self.name) - if not W.mkdir_home(path, 0o700): - message = ( - "{prefix}matrix: Error creating server session " "directory" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - - @property - def connected(self): - return self._connected - - @connected.setter - def connected(self, value): - self._connected = value - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - def get_session_path(self): - home_dir = W.info_get("weechat_dir", "") - return os.path.join(home_dir, "matrix", self.name) - - def _load_device_id(self, user=None): - user = user or self.config.username - - file_name = "{}{}".format(user, ".device_id") - path = os.path.join(self.get_session_path(), file_name) - - if not os.path.isfile(path): - return - - with open(path, "r") as device_file: - device_id = device_file.readline().rstrip() - if device_id: - self.device_id = device_id - - def save_device_id(self): - file_name = "{}{}".format(self.config.username or "main", ".device_id") - path = os.path.join(self.get_session_path(), file_name) - - with atomic_write(path, overwrite=True) as device_file: - device_file.write(self.device_id) - - @staticmethod - def _parse_url(address, port): - if not address.startswith("http"): - address = "https://{}".format(address) - - parsed_url = urlparse(address) - - homeserver = parsed_url._replace( - netloc=parsed_url.hostname + ":{}".format(port) - ) - - return homeserver - - def _change_client(self): - homeserver = MatrixServer._parse_url( - self.config.address, - self.config.port - ) - self.address = homeserver.hostname - self.homeserver = homeserver - - config = ClientConfig(store_sync_tokens=True) - - self.client = HttpClient( - homeserver.geturl(), - self.config.username, - self.device_id, - self.get_session_path(), - config=config - ) - self.client.add_to_device_callback( - self.key_verification_cb, - KeyVerificationEvent - ) - - def key_verification_cb(self, event): - if isinstance(event, KeyVerificationStart): - self.info_highlight("{user} via {device} has started a key " - "verification process.\n" - "To accept use /olm verification " - "accept {user} {device}".format( - user=event.sender, - device=event.from_device - )) - - elif isinstance(event, KeyVerificationKey): - sas = self.client.key_verifications.get(event.transaction_id, None) - if not sas: - return - - if sas.canceled: - return - - device = sas.other_olm_device - emoji = sas.get_emoji() - - emojis = [x[0] for x in emoji] - descriptions = [x[1] for x in emoji] - - centered_width = 12 - - def center_emoji(emoji, width): - # Assume each emoji has width 2 - emoji_width = 2 - - # These are emojis that need VARIATION-SELECTOR-16 (U+FE0F) so - # that they are rendered with coloured glyphs. For these, we - # need to add an extra space after them so that they are - # rendered properly in weechat. - variation_selector_emojis = [ - '☁️', - '❤️', - '☂️', - '✏️', - '✂️', - '☎️', - '✈️' - ] - - # Hack to make weechat behave properly when one of the above is - # printed. - if emoji in variation_selector_emojis: - emoji += " " - - # This is a trick to account for the fact that emojis are wider - # than other monospace characters. - placeholder = '.' * emoji_width - - return placeholder.center(width).replace(placeholder, emoji) - - emoji_str = u"".join(center_emoji(e, centered_width) - for e in emojis) - desc = u"".join(d.center(centered_width) for d in descriptions) - short_string = u"\n".join([emoji_str, desc]) - - self.info_highlight(u"Short authentication string for " - u"{user} via {device}:\n{string}\n" - u"Confirm that the strings match with " - u"/olm verification confirm {user} " - u"{device}".format( - user=device.user_id, - device=device.id, - string=short_string - )) - - elif isinstance(event, KeyVerificationMac): - try: - sas = self.client.key_verifications[event.transaction_id] - except KeyError: - return - - device = sas.other_olm_device - - if sas.verified: - self.info_highlight("Device {} of user {} successfully " - "verified".format( - device.id, - device.user_id - )) - - elif isinstance(event, KeyVerificationCancel): - self.info_highlight("The interactive device verification with " - "user {} got canceled: {}.".format( - event.sender, - event.reason - )) - - def update_option(self, option, option_name): - if option_name == "address": - self._change_client() - elif option_name == "port": - self._change_client() - elif option_name == "ssl_verify": - value = W.config_boolean(option) - if value: - self.ssl_context.verify_mode = ssl.CERT_REQUIRED - self.ssl_context.check_hostname = True - else: - self.ssl_context.check_hostname = False - self.ssl_context.verify_mode = ssl.CERT_NONE - elif option_name == "username": - value = W.config_string(option) - self.access_token = "" - - self._load_device_id() - - if self.client: - self.client.user = value - if self.device_id: - self.client.device_id = self.device_id - else: - pass - - def send_or_queue(self, request): - # type: (bytes) -> None - self.send(request) - - def try_send(self, message): - # type: (MatrixServer, bytes) -> bool - - sock = self.socket - - if not sock: - return False - - total_sent = 0 - message_length = len(message) - - while total_sent < message_length: - try: - sent = sock.send(message[total_sent:]) - - except ssl.SSLWantWriteError: - hook = W.hook_fd(sock.fileno(), 0, 1, 0, "send_cb", self.name) - self.send_fd_hook = hook - self.send_buffer = message[total_sent:] - return True - - except socket.error as error: - self._abort_send() - - errno = "error" + str(error.errno) + " " if error.errno else "" - strerr = error.strerror if error.strerror else "Unknown reason" - strerr = errno + strerr - - error_message = ( - "{prefix}Error while writing to " "socket: {error}" - ).format(prefix=W.prefix("network"), error=strerr) - - server_buffer_prnt(self, error_message) - server_buffer_prnt( - self, - ("{prefix}matrix: disconnecting from server...").format( - prefix=W.prefix("network") - ), - ) - - self.disconnect() - return False - - if sent == 0: - self._abort_send() - - server_buffer_prnt( - self, - "{prefix}matrix: Error while writing to socket".format( - prefix=W.prefix("network") - ), - ) - server_buffer_prnt( - self, - ("{prefix}matrix: disconnecting from server...").format( - prefix=W.prefix("network") - ), - ) - self.disconnect() - return False - - total_sent = total_sent + sent - - self._finalize_send() - return True - - def _abort_send(self): - self.send_buffer = b"" - - def _finalize_send(self): - # type: (MatrixServer) -> None - self.send_buffer = b"" - - def info_highlight(self, message): - buf = "" - if self.server_buffer: - buf = self.server_buffer - - msg = "{}{}: {}".format(W.prefix("network"), SCRIPT_NAME, message) - W.prnt_date_tags(buf, 0, "notify_highlight", msg) - - def info(self, message): - buf = "" - if self.server_buffer: - buf = self.server_buffer - - msg = "{}{}: {}".format(W.prefix("network"), SCRIPT_NAME, message) - W.prnt(buf, msg) - - def error(self, message): - buf = "" - if self.server_buffer: - buf = self.server_buffer - - msg = "{}{}: {}".format(W.prefix("error"), SCRIPT_NAME, message) - W.prnt(buf, msg) - - def send(self, data): - # type: (bytes) -> bool - self.try_send(data) - - return True - - def reconnect(self): - message = ("{prefix}matrix: reconnecting to server...").format( - prefix=W.prefix("network") - ) - - server_buffer_prnt(self, message) - - self.reconnect_time = None - - if not self.connect(): - self.schedule_reconnect() - - def schedule_reconnect(self): - # type: (MatrixServer) -> None - self.connecting = True - self.reconnect_time = time.time() - - if self.reconnect_delay: - self.reconnect_delay = ( - self.reconnect_delay - * G.CONFIG.network.autoreconnect_delay_growing - ) - else: - self.reconnect_delay = self.config.reconnect_delay - - if G.CONFIG.network.autoreconnect_delay_max > 0: - self.reconnect_delay = min(self.reconnect_delay, - G.CONFIG.network.autoreconnect_delay_max) - - message = ( - "{prefix}matrix: reconnecting to server in {t} " "seconds" - ).format(prefix=W.prefix("network"), t=self.reconnect_delay) - - server_buffer_prnt(self, message) - - def _close_socket(self): - # type: () -> None - if self.socket: - try: - self.socket.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - - try: - self.socket.close() - except OSError: - pass - - def disconnect(self, reconnect=True): - # type: (bool) -> None - if self.fd_hook: - W.unhook(self.fd_hook) - - self._close_socket() - - self.fd_hook = None - self.socket = None - self.connected = False - self.access_token = "" - - self.send_buffer = b"" - self.transport_type = None - self.member_request_list = [] - - if self.client: - try: - self.client.disconnect() - except LocalProtocolError: - pass - - self.lag = 0 - W.bar_item_update("lag") - self.reconnect_time = None - - # Clear our request flags. - self.keys_queried = False - self.keys_claimed = defaultdict(bool) - self.group_session_shared = defaultdict(bool) - self.ignore_while_sharing = defaultdict(bool) - self.to_device_sent = [] - - if self.server_buffer: - message = ("{prefix}matrix: disconnected from server").format( - prefix=W.prefix("network") - ) - server_buffer_prnt(self, message) - - if reconnect: - self.schedule_reconnect() - else: - self.reconnect_delay = 0 - - def connect(self): - # type: (MatrixServer) -> int - if not self.config.address or not self.config.port: - message = "{prefix}Server address or port not set".format( - prefix=W.prefix("error") - ) - W.prnt("", message) - return False - - if self.connected: - return True - - if not self.server_buffer: - create_server_buffer(self) - - if not self.timer_hook: - self.timer_hook = W.hook_timer( - 1 * 1000, 0, 0, "matrix_timer_cb", self.name - ) - - ssl_message = " (SSL)" if self.ssl_context.check_hostname else "" - - message = ( - "{prefix}matrix: Connecting to " "{server}:{port}{ssl}..." - ).format( - prefix=W.prefix("network"), - server=self.address, - port=self.config.port, - ssl=ssl_message, - ) - - W.prnt(self.server_buffer, message) - - W.hook_connect( - self.config.proxy, - self.address, - self.config.port, - 1, - 0, - "", - "connect_cb", - self.name, - ) - - return True - - def schedule_sync(self): - self.sync_time = time.time() - - def sync(self, timeout=None, sync_filter=None): - # type: (Optional[int], Optional[Dict[Any, Any]]) -> None - if not self.client: - return - - self.sync_time = None - _, request = self.client.sync(timeout, sync_filter, - full_state=self.first_sync) - - self.send_or_queue(request) - - def login_info(self): - # type: () -> None - if not self.client: - return - - if self.client.logged_in: - self.login() - return - - _, request = self.client.login_info() - self.send(request) - - """Start a local HTTP server to listen for SSO tokens.""" - def start_login_sso(self): - # type: () -> None - if self.sso_hook: - # If there is a stale SSO process hanging around kill it. We could - # let it stay around but the URL that needs to be opened by the - # user is printed out in the callback. - W.hook_set(self.sso_hook, "signal", "term") - self.sso_hook = None - - process_args = { - "buffer_flush": "1", - "arg1": "--port", - "arg2": str(self.config.sso_helper_listening_port) - } - - self.sso_hook = W.hook_process_hashtable( - "matrix_sso_helper", - process_args, - 0, - "sso_login_cb", - self.name - ) - - def login(self, token=None): - # type: (...) -> None - assert self.client is not None - if self.client.logged_in: - msg = ( - "{prefix}{script_name}: Already logged in, " "syncing..." - ).format(prefix=W.prefix("network"), script_name=SCRIPT_NAME) - W.prnt(self.server_buffer, msg) - timeout = 0 if self.transport_type == TransportType.HTTP else 30000 - limit = (G.CONFIG.network.max_initial_sync_events if self.first_sync else 500) - sync_filter = { - "room": { - "timeline": {"limit": limit}, - "state": {"lazy_load_members": True} - } - } - self.sync(timeout, sync_filter) - return - - if (not self.config.username or not self.config.password) and not token: - message = "{prefix}User or password not set".format( - prefix=W.prefix("error") - ) - W.prnt("", message) - return self.disconnect() - - if token: - _, request = self.client.login( - device_name=self.config.device_name, token=token - ) - else: - _, request = self.client.login( - password=self.config.password, device_name=self.config.device_name - ) - self.send_or_queue(request) - - msg = "{prefix}matrix: Logging in...".format( - prefix=W.prefix("network") - ) - - W.prnt(self.server_buffer, msg) - - def devices(self): - _, request = self.client.devices() - self.send_or_queue(request) - - def delete_device(self, device_id, auth=None): - uuid, request = self.client.delete_devices([device_id], auth) - self.device_deletion_queue[uuid] = device_id - self.send_or_queue(request) - return - - def rename_device(self, device_id, display_name): - content = { - "display_name": display_name - } - - _, request = self.client.update_device(device_id, content) - self.send_or_queue(request) - - def room_send_state(self, room_buffer, body, event_type): - _, request = self.client.room_put_state( - room_buffer.room.room_id, event_type, body - ) - self.send_or_queue(request) - - def room_send_redaction(self, room_buffer, event_id, reason=None): - _, request = self.client.room_redact( - room_buffer.room.room_id, event_id, reason - ) - self.send_or_queue(request) - - def room_kick(self, room_buffer, user_id, reason=None): - _, request = self.client.room_kick( - room_buffer.room.room_id, user_id, reason - ) - self.send_or_queue(request) - - def room_invite(self, room_buffer, user_id): - _, request = self.client.room_invite(room_buffer.room.room_id, user_id) - self.send_or_queue(request) - - def room_join(self, room_id): - _, request = self.client.join(room_id) - self.send_or_queue(request) - - def room_leave(self, room_id): - _, request = self.client.room_leave(room_id) - self.send_or_queue(request) - - def room_get_messages(self, room_id): - if not self.connected or not self.client.logged_in: - return False - - room_buffer = self.find_room_from_id(room_id) - - # We're already fetching old messages - if room_buffer.backlog_pending: - return False - - if not room_buffer.prev_batch: - return False - - uuid, request = self.client.room_messages( - room_id, - room_buffer.prev_batch, - limit=10) - - room_buffer.backlog_pending = True - self.backlog_queue[uuid] = room_id - self.send_or_queue(request) - - return True - - def room_send_read_marker(self, room_id, event_id): - """Send read markers for the provided room. - - Args: - room_id(str): the room for which the read markers should - be sent. - event_id(str): the event id where to set the marker - """ - if not self.connected or not self.client.logged_in: - return - - _, request = self.client.room_read_markers( - room_id, - fully_read_event=event_id, - read_event=event_id) - self.send(request) - - def room_send_typing_notice(self, room_buffer): - """Send a typing notice for the provided room. - - Args: - room_buffer(RoomBuffer): the room for which the typing notice needs - to be sent. - """ - if not self.connected or not self.client.logged_in: - return - - input = room_buffer.weechat_buffer.input - - typing_enabled = bool(int(W.string_eval_expression( - G.CONFIG.network.typing_notice_conditions, - {}, - {"typing_enabled": str(int(room_buffer.typing_enabled))}, - {"type": "condition"} - ))) - - if not typing_enabled: - return - - # Don't send a typing notice if the user is typing in a weechat command - if input.startswith("/") and not input.startswith("//"): - return - - # Don't send a typing notice if we only typed a couple of letters. - elif len(input) < 4 and not room_buffer.typing: - return - - # If we were typing already and our input bar now has no letters or - # only a couple of letters stop the typing notice. - elif len(input) < 4: - _, request = self.client.room_typing( - room_buffer.room.room_id, - typing_state=False) - room_buffer.typing = False - self.send(request) - return - - # Don't send out a typing notice if we already sent one out and it - # didn't expire yet. - if not room_buffer.typing_notice_expired: - return - - _, request = self.client.room_typing( - room_buffer.room.room_id, - typing_state=True, - timeout=TYPING_NOTICE_TIMEOUT) - - room_buffer.typing = True - self.send(request) - - def room_send_upload( - self, - upload - ): - """Send a room message containing the mxc URI of an upload.""" - try: - room_buffer = self.find_room_from_id(upload.room_id) - except (ValueError, KeyError): - return True - - assert self.client - - if room_buffer.room.encrypted: - assert upload.encrypt - - content = upload.content - - try: - uuid = self.room_send_event(upload.room_id, content) - except (EncryptionError, GroupEncryptionError): - message = EncryptionQueueItem(upload.msgtype, upload) - self.encryption_queue[upload.room_id].append(message) - return False - - attributes = DEFAULT_ATTRIBUTES.copy() - formatted = Formatted([FormattedString( - upload.render, - attributes - )]) - - own_message = OwnMessage( - self.user_id, 0, "", uuid, upload.room_id, formatted - ) - - room_buffer.sent_messages_queue[uuid] = own_message - self.print_unconfirmed_message(room_buffer, own_message) - - return True - - def share_group_session( - self, - room_id, - ignore_missing_sessions=False, - ignore_unverified_devices=False - ): - - self.ignore_while_sharing[room_id] = ignore_unverified_devices - - _, request = self.client.share_group_session( - room_id, - ignore_missing_sessions=ignore_missing_sessions, - ignore_unverified_devices=ignore_unverified_devices - ) - self.send(request) - self.group_session_shared[room_id] = True - - def room_send_event( - self, - room_id, # type: str - content, # type: Dict[str, str] - event_type="m.room.message", # type: str - ignore_unverified_devices=False, # type: bool - ): - # type: (...) -> UUID - assert self.client - - try: - uuid, request = self.client.room_send( - room_id, event_type, content - ) - self.send(request) - return uuid - except GroupEncryptionError: - try: - if not self.group_session_shared[room_id]: - self.share_group_session( - room_id, - ignore_unverified_devices=ignore_unverified_devices - ) - raise - - except EncryptionError: - if not self.keys_claimed[room_id]: - _, request = self.client.keys_claim(room_id) - self.keys_claimed[room_id] = True - self.send(request) - raise - - def room_send_message( - self, - room_buffer, # type: RoomBuffer - formatted, # type: Formatted - msgtype="m.text", # type: str - ignore_unverified_devices=False, # type: bool - in_reply_to_event_id="", # type: str - ): - # type: (...) -> bool - room = room_buffer.room - - assert self.client - - content = {"msgtype": msgtype, "body": formatted.to_plain()} - - if formatted.is_formatted() or in_reply_to_event_id: - content["format"] = "org.matrix.custom.html" - content["formatted_body"] = formatted.to_html() - if in_reply_to_event_id: - content["m.relates_to"] = { - "m.in_reply_to": {"event_id": in_reply_to_event_id} - } - - try: - uuid = self.room_send_event( - room.room_id, - content, - ignore_unverified_devices=ignore_unverified_devices - ) - except (EncryptionError, GroupEncryptionError): - message = EncryptionQueueItem(msgtype, formatted) - self.encryption_queue[room.room_id].append(message) - return False - - if msgtype == "m.emote": - message_class = OwnAction # type: Type - else: - message_class = OwnMessage - - own_message = message_class( - self.user_id, 0, "", uuid, room.room_id, formatted - ) - - room_buffer.sent_messages_queue[uuid] = own_message - self.print_unconfirmed_message(room_buffer, own_message) - - return True - - def print_unconfirmed_message(self, room_buffer, message): - """Print an outgoing message before getting a receive confirmation. - - The message is printed out greyed out and only printed out if the - client is configured to do so. The message needs to be later modified - to contain proper coloring, this is done in the - replace_printed_line_by_uuid() method of the RoomBuffer class. - - Args: - room_buffer(RoomBuffer): the buffer of the room where the message - needs to be printed out - message(OwnMessages): the message that should be printed out - """ - if G.CONFIG.network.print_unconfirmed_messages: - room_buffer.printed_before_ack_queue.append(message.uuid) - plain_message = message.formatted_message.to_weechat() - plain_message = W.string_remove_color(plain_message, "") - attributes = DEFAULT_ATTRIBUTES.copy() - attributes["fgcolor"] = G.CONFIG.color.unconfirmed_message_fg - attributes["bgcolor"] = G.CONFIG.color.unconfirmed_message_bg - new_formatted = Formatted([FormattedString( - plain_message, - attributes - )]) - - new_message = copy.copy(message) - new_message.formatted_message = new_formatted - - if isinstance(new_message, OwnAction): - room_buffer.self_action(new_message) - elif isinstance(new_message, OwnMessage): - room_buffer.self_message(new_message) - - def keys_upload(self): - _, request = self.client.keys_upload() - self.send_or_queue(request) - - def keys_query(self): - _, request = self.client.keys_query() - self.keys_queried = True - self.send_or_queue(request) - - def get_joined_members(self, room_id): - if not self.connected or not self.client.logged_in: - return - - if room_id in self.member_request_list: - return - - self.member_request_list.append(room_id) - _, request = self.client.joined_members(room_id) - self.send(request) - - def _print_message_error(self, message): - server_buffer_prnt( - self, - ( - "{prefix}Unhandled {status_code} error, please " - "inform the developers about this." - ).format( - prefix=W.prefix("error"), status_code=message.response.status - ), - ) - - server_buffer_prnt(self, pprint.pformat(message.__class__.__name__)) - server_buffer_prnt(self, pprint.pformat(message.request.payload)) - server_buffer_prnt(self, pprint.pformat(message.response.body)) - - def handle_own_messages_error(self, response): - room_buffer = self.room_buffers[response.room_id] - - if response.uuid not in room_buffer.printed_before_ack_queue: - return - - message = room_buffer.sent_messages_queue.pop(response.uuid) - room_buffer.mark_message_as_unsent(response.uuid, message) - room_buffer.printed_before_ack_queue.remove(response.uuid) - - def handle_own_messages(self, response): - def send_marker(): - if not room_buffer.read_markers_enabled: - return - - self.room_send_read_marker(response.room_id, response.event_id) - room_buffer.last_read_event = response.event_id - - room_buffer = self.room_buffers[response.room_id] - - message = room_buffer.sent_messages_queue.pop(response.uuid, None) - - # The message might have been returned in a sync response before we got - # a room send response. - if not message: - return - - message.event_id = response.event_id - # We already printed the message, just modify it to contain the proper - # colors and formatting. - if response.uuid in room_buffer.printed_before_ack_queue: - room_buffer.replace_printed_line_by_uuid(response.uuid, message) - room_buffer.printed_before_ack_queue.remove(response.uuid) - send_marker() - return - - if isinstance(message, OwnAction): - room_buffer.self_action(message) - send_marker() - return - if isinstance(message, OwnMessage): - room_buffer.self_message(message) - send_marker() - return - - raise NotImplementedError( - "Unsupported message of type {}".format(type(message)) - ) - - def handle_backlog_response(self, response): - room_id = self.backlog_queue.pop(response.uuid) - room_buffer = self.find_room_from_id(room_id) - room_buffer.first_view = False - - room_buffer.handle_backlog(response) - - def handle_devices_response(self, response): - if not response.devices: - m = "{}{}: No devices found for this account".format( - W.prefix("error"), - SCRIPT_NAME) - W.prnt(self.server_buffer, m) - - header = (W.prefix("network") + SCRIPT_NAME + ": Devices for " - "server {}{}{}:\n" - " Device ID Device Name " - "Last Seen").format( - W.color("chat_server"), - self.name, - W.color("reset") - ) - W.prnt(self.server_buffer, header) - - lines = [] - for device in response.devices: - last_seen_date = ("?" if not device.last_seen_date else - device.last_seen_date.strftime("%Y/%m/%d %H:%M")) - last_seen = "{ip} @ {date}".format( - ip=device.last_seen_ip or "?", - date=last_seen_date - ) - device_color = ("chat_self" if device.id == self.device_id else - W.info_get("nick_color_name", device.id)) - bold = W.color("bold") if device.id == self.device_id else "" - line = " {}{}{:<18}{}{:<34}{:<}".format( - bold, - W.color(device_color), - device.id, - W.color("resetcolor"), - device.display_name or "", - last_seen - ) - lines.append(line) - W.prnt(self.server_buffer, "\n".join(lines)) - - """Handle a login info response and chose one of the available flows - - This currently supports only SSO and password logins. If both are available - password takes precedence over SSO if a username and password is provided. - - """ - def _handle_login_info(self, response): - if ("m.login.sso" in response.flows - and (not self.config.username or not self.config.password)): - self.start_login_sso() - elif "m.login.password" in response.flows: - self.login() - else: - self.error("No supported login flow found") - self.disconnect() - - def _handle_login(self, response): - self.access_token = response.access_token - self.user_id = response.user_id - self.client.access_token = response.access_token - self.device_id = response.device_id - self.save_device_id() - - message = "{prefix}matrix: Logged in as {user}".format( - prefix=W.prefix("network"), user=self.user_id - ) - - W.prnt(self.server_buffer, message) - - if not self.client.olm_account_shared: - self.keys_upload() - - sync_filter = { - "room": { - "timeline": { - "limit": G.CONFIG.network.max_initial_sync_events - }, - "state": {"lazy_load_members": True} - } - } - self.sync(timeout=0, sync_filter=sync_filter) - - def _handle_room_info(self, response): - for room_id, info in response.rooms.invite.items(): - room = self.client.invited_rooms.get(room_id, None) - - if room: - if room.inviter: - inviter_msg = " by {}{}".format( - W.color("chat_nick_other"), room.inviter - ) - else: - inviter_msg = "" - - self.info_highlight( - "You have been invited to {} {}({}{}{}){}" - "{}".format( - room.display_name, - W.color("chat_delimiters"), - W.color("chat_channel"), - room_id, - W.color("chat_delimiters"), - W.color("reset"), - inviter_msg, - ) - ) - else: - self.info_highlight("You have been invited to {}.".format( - room_id - )) - - for room_id, info in response.rooms.leave.items(): - if room_id not in self.buffers: - continue - - room_buffer = self.find_room_from_id(room_id) - room_buffer.handle_left_room(info) - - for room_id, info in response.rooms.join.items(): - if room_id not in self.buffers: - self.create_room_buffer(room_id, info.timeline.prev_batch) - - room_buffer = self.find_room_from_id(room_id) - room_buffer.handle_joined_room(info) - - def add_unhandled_users(self, rooms, n): - # type: (List[RoomBuffer], int) -> bool - total_users = 0 - - while total_users <= n: - try: - room_buffer = rooms.pop() - except IndexError: - return False - - handled_users = 0 - - users = room_buffer.unhandled_users - - for user_id in users: - room_buffer.add_user(user_id, 0, True) - handled_users += 1 - total_users += 1 - - if total_users >= n: - room_buffer.unhandled_users = users[handled_users:] - rooms.append(room_buffer) - return True - - room_buffer.unhandled_users = [] - - return False - - def _hook_lazy_user_adding(self): - if not self.lazy_load_hook: - hook = W.hook_timer(1 * 1000, 0, 0, - "matrix_load_users_cb", self.name) - self.lazy_load_hook = hook - - def decrypt_printed_messages(self, key_event): - """Decrypt already printed messages and send them to the buffer""" - try: - room_buffer = self.find_room_from_id(key_event.room_id) - except KeyError: - return - - decrypted_events = [] - - for undecrypted_event in room_buffer.undecrypted_events: - if undecrypted_event.session_id != key_event.session_id: - continue - - event = self.client.decrypt_event(undecrypted_event) - if event: - decrypted_events.append((undecrypted_event, event)) - - for event_pair in decrypted_events: - undecrypted_event, event = event_pair - room_buffer.undecrypted_events.remove(undecrypted_event) - room_buffer.replace_undecrypted_line(event) - - def start_verification(self, device): - _, request = self.client.start_key_verification(device) - self.send(request) - self.info("Starting an interactive device verification with " - "{} {}".format(device.user_id, device.id)) - - def accept_sas(self, sas): - _, request = self.client.accept_key_verification(sas.transaction_id) - self.send(request) - - def cancel_sas(self, sas): - _, request = self.client.cancel_key_verification(sas.transaction_id) - self.send(request) - - def to_device(self, message): - _, request = self.client.to_device(message) - self.send(request) - - def confirm_sas(self, sas): - _, request = self.client.confirm_short_auth_string(sas.transaction_id) - self.send(request) - - device = sas.other_olm_device - - if sas.verified: - self.info("Device {} of user {} successfully verified".format( - device.id, - device.user_id - )) - else: - self.info("Waiting for {} to confirm...".format(device.user_id)) - - def _handle_sync(self, response): - # we got the same batch again, nothing to do - self.first_sync = False - - if self.next_batch == response.next_batch: - self.schedule_sync() - return - - self._handle_room_info(response) - - for event in response.to_device_events: - if isinstance(event, RoomKeyEvent): - message = { - "sender": event.sender, - "sender_key": event.sender_key, - "room_id": event.room_id, - "session_id": event.session_id, - "algorithm": event.algorithm, - "server": self.name, - } - W.hook_hsignal_send("matrix_room_key_received", message) - - # TODO try to decrypt some cached undecrypted messages with the - # new key - # self.decrypt_printed_messages(event) - - if self.client.should_upload_keys: - self.keys_upload() - - if self.client.should_query_keys and not self.keys_queried: - self.keys_query() - - for room_buffer in self.room_buffers.values(): - # It's our initial sync, we need to fetch room members, so add - # the room to the missing members queue. - # 3 reasons we fetch room members here: - # * If the lazy load room users setting is off, otherwise we will - # fetch them when we switch to the buffer - # * If the room is encrypted, encryption needs the full member - # list for it to work. - # * If we are the only member, it is unlikely really an empty - # room and since we don't want a bunch of "Empty room?" - # buffers in our buffer list we fetch members here. - if not self.next_batch: - if (not G.CONFIG.network.lazy_load_room_users - or room_buffer.room.encrypted - or room_buffer.room.member_count <= 1): - self.rooms_with_missing_members.append( - room_buffer.room.room_id - ) - if room_buffer.unhandled_users: - self._hook_lazy_user_adding() - break - - self.next_batch = response.next_batch - self.schedule_sync() - W.bar_item_update("matrix_typing_notice") - - if self.rooms_with_missing_members: - self.get_joined_members(self.rooms_with_missing_members.pop()) - - def handle_delete_device_auth(self, response): - device_id = self.device_deletion_queue.pop(response.uuid, None) - - if not device_id: - return - - for flow in response.flows: - if "m.login.password" in flow["stages"]: - session = response.session - auth = { - "type": "m.login.password", - "session": session, - "user": self.client.user_id, - "password": self.config.password - } - self.delete_device(device_id, auth) - return - - self.error("No supported auth method for device deletion found.") - - def handle_error_response(self, response): - self.error("Error: {}".format(str(response))) - - if isinstance(response, (SyncError, LoginError)): - self.disconnect() - elif isinstance(response, JoinedMembersError): - self.rooms_with_missing_members.append(response.room_id) - self.get_joined_members(self.rooms_with_missing_members.pop()) - elif isinstance(response, RoomSendError): - self.handle_own_messages_error(response) - elif isinstance(response, ShareGroupSessionError): - self.group_session_shared[response.room_id] = False - self.share_group_session( - response.room_id, - False, - self.ignore_while_sharing[response.room_id] - ) - - elif isinstance(response, ToDeviceError): - try: - self.to_device_sent.remove(response.to_device_message) - except ValueError: - pass - - def handle_response(self, response): - # type: (Response) -> None - response_lag = response.elapsed - - current_lag = 0 - - if self.client: - current_lag = self.client.lag - - if response_lag >= current_lag: - self.lag = response_lag * 1000 - self.lag_done = True - W.bar_item_update("lag") - - if isinstance(response, ErrorResponse): - self.handle_error_response(response) - if isinstance(response, RoomMessagesError): - room_buffer = self.room_buffers[response.room_id] - room_buffer.backlog_pending = False - - elif isinstance(response, ToDeviceResponse): - try: - self.to_device_sent.remove(response.to_device_message) - except ValueError: - pass - - elif isinstance(response, LoginResponse): - self._handle_login(response) - - elif isinstance(response, LoginInfoResponse): - self._handle_login_info(response) - - elif isinstance(response, SyncResponse): - self._handle_sync(response) - - elif isinstance(response, RoomSendResponse): - self.handle_own_messages(response) - - elif isinstance(response, RoomMessagesResponse): - self.handle_backlog_response(response) - - elif isinstance(response, DevicesResponse): - self.handle_devices_response(response) - - elif isinstance(response, UpdateDeviceResponse): - self.info("Device name successfully updated") - - elif isinstance(response, DeleteDevicesAuthResponse): - self.handle_delete_device_auth(response) - - elif isinstance(response, DeleteDevicesResponse): - self.info("Device successfully deleted") - - elif isinstance(response, KeysQueryResponse): - self.keys_queried = False - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - for user_id, device_dict in response.changed.items(): - for device in device_dict.values(): - message = { - "user_id": user_id, - "device_id": device.id, - "ed25519": device.ed25519, - "curve25519": device.curve25519, - "deleted": str(device.deleted) - } - W.hook_hsignal_send("matrix_device_changed", message) - - elif isinstance(response, JoinedMembersResponse): - self.member_request_list.remove(response.room_id) - room_buffer = self.room_buffers[response.room_id] - users = [user.user_id for user in response.members] - - # Don't add the users directly use the lazy load hook. - room_buffer.unhandled_users += users - self._hook_lazy_user_adding() - room_buffer.members_fetched = True - room_buffer.update_buffer_name() - - # Fetch the users for the next room. - if self.rooms_with_missing_members: - self.get_joined_members(self.rooms_with_missing_members.pop()) - # We are done adding all the users, do a full key query now since - # the client knows all the encrypted room members. - else: - if self.client.should_query_keys and not self.keys_queried: - self.keys_query() - - elif isinstance(response, KeysClaimResponse): - self.keys_claimed[response.room_id] = False - try: - self.share_group_session( - response.room_id, - True, - self.ignore_while_sharing[response.room_id] - ) - except OlmTrustError as e: - m = ("Untrusted devices found in room: {}".format(e)) - room_buffer = self.find_room_from_id(response.room_id) - room_buffer.error(m) - - try: - item = self.encryption_queue[response.room_id][0] - if item.message_type not in ["m.file", "m.video", - "m.audio", "m.image"]: - room_buffer.last_message = item.message - except IndexError: - pass - - self.encryption_queue[response.room_id].clear() - return - - elif isinstance(response, ShareGroupSessionResponse): - room_id = response.room_id - self.group_session_shared[response.room_id] = False - ignore_unverified = self.ignore_while_sharing[response.room_id] - self.ignore_while_sharing[response.room_id] = False - - room_buffer = self.room_buffers[room_id] - - while self.encryption_queue[room_id]: - item = self.encryption_queue[room_id].popleft() - try: - if item.message_type in [ - "m.file", - "m.video", - "m.audio", - "m.image" - ]: - ret = self.room_send_upload(item.message) - else: - assert isinstance(item.message, Formatted) - ret = self.room_send_message( - room_buffer, - item.message, - item.message_type, - ignore_unverified_devices=ignore_unverified - ) - - if not ret: - self.encryption_queue[room_id].pop() - self.encryption_queue[room_id].appendleft(item) - break - - except OlmTrustError: - self.encryption_queue[room_id].clear() - - # If the item is a normal user message store it in the - # buffer to enable the send-anyways functionality. - if item.message_type not in ["m.file", "m.video", - "m.audio", "m.image"]: - room_buffer.last_message = item.message - - break - - def create_room_buffer(self, room_id, prev_batch): - room = self.client.rooms[room_id] - buf = RoomBuffer(room, self.name, self.homeserver, prev_batch) - - # We sadly don't get a correct summary on full_state from synapse so we - # can't trust it that the members are fully synced - # if room.members_synced: - # buf.members_fetched = True - - self.room_buffers[room_id] = buf - self.buffers[room_id] = buf.weechat_buffer._ptr - - def find_room_from_ptr(self, pointer): - try: - room_id = key_from_value(self.buffers, pointer) - room_buffer = self.room_buffers[room_id] - - return room_buffer - except (ValueError, KeyError): - return None - - def find_room_from_id(self, room_id): - room_buffer = self.room_buffers[room_id] - return room_buffer - - def garbage_collect_users(self): - """ Remove inactive users. - This tries to keep the number of users added to the nicklist less than - the configuration option matrix.network.max_nicklist_users. It - removes users that have not been active for a day until there are - less than max_nicklist_users or no users are left for removal. - It never removes users that have a bigger power level than the - default one. - This function is run every hour by the server timer callback""" - - now = time.time() - self.user_gc_time = now - - def day_passed(t1, t2): - return (t2 - t1) > 86400 - - for room_buffer in self.room_buffers.values(): - to_remove = max( - (len(room_buffer.displayed_nicks) - - G.CONFIG.network.max_nicklist_users), - 0 - ) - - if not to_remove: - continue - - removed = 0 - removed_user_ids = [] - - for user_id, nick in room_buffer.displayed_nicks.items(): - user = room_buffer.weechat_buffer.users[nick] - - if (not user.speaking_time or - day_passed(user.speaking_time, now)): - room_buffer.weechat_buffer.part(nick, 0, False) - removed_user_ids.append(user_id) - removed += 1 - - if removed >= to_remove: - break - - for user_id in removed_user_ids: - del room_buffer.displayed_nicks[user_id] - - def buffer_merge(self): - if not self.server_buffer: - return - - buf = self.server_buffer - - if G.CONFIG.look.server_buffer == ServerBufferType.MERGE_CORE: - num = W.buffer_get_integer(W.buffer_search_main(), "number") - W.buffer_unmerge(buf, num + 1) - W.buffer_merge(buf, W.buffer_search_main()) - elif G.CONFIG.look.server_buffer == ServerBufferType.MERGE: - if SERVERS: - first = None - for server in SERVERS.values(): - if server.server_buffer: - first = server.server_buffer - break - if first: - num = W.buffer_get_integer( - W.buffer_search_main(), "number" - ) - W.buffer_unmerge(buf, num + 1) - if buf is not first: - W.buffer_merge(buf, first) - else: - num = W.buffer_get_integer(W.buffer_search_main(), "number") - W.buffer_unmerge(buf, num + 1) - - -@utf8_decode -def matrix_config_server_read_cb( - data, config_file, section, option_name, value -): - - return_code = W.WEECHAT_CONFIG_OPTION_SET_ERROR - - if option_name: - server_name, option = option_name.rsplit(".", 1) - server = None - - if server_name in SERVERS: - server = SERVERS[server_name] - else: - server = MatrixServer(server_name, config_file) - SERVERS[server.name] = server - - # Ignore invalid options - if option in server.config._option_ptrs: - return_code = W.config_option_set( - server.config._option_ptrs[option], value, 1 - ) - - # TODO print out error message in case of erroneous return_code - - return return_code - - -@utf8_decode -def matrix_config_server_write_cb(data, config_file, section_name): - if not W.config_write_line(config_file, section_name, ""): - return W.WEECHAT_CONFIG_WRITE_ERROR - - for server in SERVERS.values(): - for option in server.config._option_ptrs.values(): - if not W.config_write_option(config_file, option): - return W.WEECHAT_CONFIG_WRITE_ERROR - - return W.WEECHAT_CONFIG_WRITE_OK - - -@utf8_decode -def matrix_config_server_change_cb(server_name, option): - # type: (str, str) -> int - server = SERVERS[server_name] - option_name = None - - # The function config_option_get_string() is used to get differing - # properties from a config option, sadly it's only available in the plugin - # API of weechat. - option_name = key_from_value(server.config._option_ptrs, option) - server.update_option(option, option_name) - - return 1 - - -@utf8_decode -def matrix_load_users_cb(server_name, remaining_calls): - server = SERVERS[server_name] - start = time.time() - - rooms = [x for x in server.room_buffers.values() if x.unhandled_users] - - while server.add_unhandled_users(rooms, 100): - current = time.time() - - if current - start >= 0.1: - return W.WEECHAT_RC_OK - - # We are done adding users, we can unhook now. - W.unhook(server.lazy_load_hook) - server.lazy_load_hook = None - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_timer_cb(server_name, remaining_calls): - server = SERVERS[server_name] - - current_time = time.time() - - if ( - (not server.connected) - and server.reconnect_time - and current_time >= (server.reconnect_time + server.reconnect_delay) - ): - server.reconnect() - return W.WEECHAT_RC_OK - - if not server.connected or not server.client.logged_in: - return W.WEECHAT_RC_OK - - # check lag, disconnect if it's too big - server.lag = server.client.lag * 1000 - server.lag_done = False - W.bar_item_update("lag") - - if server.lag > G.CONFIG.network.lag_reconnect * 1000: - server.disconnect() - return W.WEECHAT_RC_OK - - for i, message in enumerate(server.client.outgoing_to_device_messages): - if i >= 5: - break - - if message in server.to_device_sent: - continue - - server.to_device(message) - server.to_device_sent.append(message) - - if server.sync_time and current_time > server.sync_time: - timeout = 0 if server.transport_type == TransportType.HTTP else 30000 - sync_filter = { - "room": { - "timeline": {"limit": 500}, - "state": {"lazy_load_members": True} - } - } - server.sync(timeout, sync_filter) - - if current_time > (server.user_gc_time + 3600): - server.garbage_collect_users() - - return W.WEECHAT_RC_OK - - -def create_default_server(config_file): - server = MatrixServer("matrix_org", config_file._ptr) - SERVERS[server.name] = server - - option = W.config_get(SCRIPT_NAME + ".server." + server.name + ".address") - W.config_option_set(option, "matrix.org", 1) - - return True - - -@utf8_decode -def send_cb(server_name, file_descriptor): - # type: (str, int) -> int - - server = SERVERS[server_name] - - if server.send_fd_hook: - W.unhook(server.send_fd_hook) - server.send_fd_hook = None - - if server.send_buffer: - server.try_send(server.send_buffer) - - return W.WEECHAT_RC_OK diff --git a/weechat/python/matrix/uploads.py b/weechat/python/matrix/uploads.py deleted file mode 100644 index 0b4e1f8..0000000 --- a/weechat/python/matrix/uploads.py +++ /dev/null @@ -1,391 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -"""Module implementing upload functionality.""" - -from __future__ import unicode_literals - -import attr -import time -import json -from typing import Dict, Any -from uuid import uuid1, UUID -from enum import Enum - -try: - from json.decoder import JSONDecodeError -except ImportError: - JSONDecodeError = ValueError # type: ignore - -from .globals import SCRIPT_NAME, SERVERS, W, UPLOADS -from .utf import utf8_decode -from .message_renderer import Render -from matrix import globals as G -from nio import Api - - -class UploadState(Enum): - created = 0 - active = 1 - finished = 2 - error = 3 - aborted = 4 - - -@attr.s -class Proxy(object): - ptr = attr.ib(type=str) - - @property - def name(self): - return W.infolist_string(self.ptr, "name") - - @property - def address(self): - return W.infolist_string(self.ptr, "address") - - @property - def type(self): - return W.infolist_string(self.ptr, "type_string") - - @property - def port(self): - return str(W.infolist_integer(self.ptr, "port")) - - @property - def user(self): - return W.infolist_string(self.ptr, "username") - - @property - def password(self): - return W.infolist_string(self.ptr, "password") - - -@attr.s -class Upload(object): - """Class representing an upload to a matrix server.""" - - server_name = attr.ib(type=str) - server_address = attr.ib(type=str) - access_token = attr.ib(type=str) - room_id = attr.ib(type=str) - filepath = attr.ib(type=str) - encrypt = attr.ib(type=bool, default=False) - file_keys = attr.ib(type=Dict, default=None) - - done = 0 - total = 0 - - uuid = None - buffer = None - upload_hook = None - content_uri = None - file_name = None - mimetype = "?" - state = UploadState.created - - def __attrs_post_init__(self): - self.uuid = uuid1() - self.buffer = "" - - server = SERVERS[self.server_name] - - proxy_name = server.config.proxy - proxy = None - proxies_list = None - - if proxy_name: - proxies_list = W.infolist_get("proxy", "", proxy_name) - if proxies_list: - W.infolist_next(proxies_list) - proxy = Proxy(proxies_list) - - process_args = { - "arg1": self.filepath, - "arg2": self.server_address, - "arg3": self.access_token, - "buffer_flush": "1", - } - - arg_count = 3 - - if self.encrypt: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--encrypt" - - if not server.config.ssl_verify: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--insecure" - - if proxy: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-type" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.type - - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-address" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.address - - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-port" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.port - - if proxy.user: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-user" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.user - - if proxy.password: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-password" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.password - - self.upload_hook = W.hook_process_hashtable( - "matrix_upload", - process_args, - 0, - "upload_cb", - str(self.uuid) - ) - - if proxies_list: - W.infolist_free(proxies_list) - - def abort(self): - pass - - @property - def msgtype(self): - # type: () -> str - assert self.mimetype - return Api.mimetype_to_msgtype(self.mimetype) - - @property - def content(self): - # type: () -> Dict[Any, Any] - assert self.content_uri - - if self.encrypt: - content = { - "body": self.file_name, - "msgtype": self.msgtype, - "file": self.file_keys, - } - content["file"]["url"] = self.content_uri - content["file"]["mimetype"] = self.mimetype - - # TODO thumbnail if it's an image - - return content - - return { - "msgtype": self.msgtype, - "body": self.file_name, - "url": self.content_uri, - } - - @property - def render(self): - # type: () -> str - assert self.content_uri - - if self.encrypt: - return Render.encrypted_media( - self.content_uri, - self.file_name, - self.file_keys["key"]["k"], - self.file_keys["hashes"]["sha256"], - self.file_keys["iv"], - ) - - return Render.media(self.content_uri, self.file_name) - - -@attr.s -class UploadsBuffer(object): - """Weechat buffer showing the uploads for a server.""" - - _ptr = "" # type: str - _selected_line = 0 # type: int - uploads = UPLOADS - - def __attrs_post_init__(self): - self._ptr = W.buffer_new( - SCRIPT_NAME + ".uploads", - "", - "", - "", - "", - ) - W.buffer_set(self._ptr, "type", "free") - W.buffer_set(self._ptr, "title", "Upload list") - W.buffer_set(self._ptr, "key_bind_meta2-A", "/uploads up") - W.buffer_set(self._ptr, "key_bind_meta2-B", "/uploads down") - W.buffer_set(self._ptr, "localvar_set_type", "uploads") - - self.render() - - def move_line_up(self): - self._selected_line = max(self._selected_line - 1, 0) - self.render() - - def move_line_down(self): - self._selected_line = min( - self._selected_line + 1, - len(self.uploads) - 1 - ) - self.render() - - def display(self): - """Display the buffer.""" - W.buffer_set(self._ptr, "display", "1") - - def render(self): - """Render the new state of the upload buffer.""" - # This function is under the MIT license. - # Copyright (c) 2016 Vladimir Ignatev - def progress(count, total): - bar_len = 60 - - if total == 0: - bar = '-' * bar_len - return "[{}] {}%".format(bar, "?") - - filled_len = int(round(bar_len * count / float(total))) - percents = round(100.0 * count / float(total), 1) - bar = '=' * filled_len + '-' * (bar_len - filled_len) - - return "[{}] {}%".format(bar, percents) - - W.buffer_clear(self._ptr) - header = "{}{}{}{}{}{}{}{}".format( - W.color("green"), - "Actions (letter+enter):", - W.color("lightgreen"), - " [A] Accept", - " [C] Cancel", - " [R] Remove", - " [P] Purge finished", - " [Q] Close this buffer" - ) - W.prnt_y(self._ptr, 0, header) - - for line_number, upload in enumerate(self.uploads.values()): - line_color = "{},{}".format( - "white" if line_number == self._selected_line else "default", - "blue" if line_number == self._selected_line else "default", - ) - first_line = ("%s%s %-24s %s%s%s %s (%s.%s)" % ( - W.color(line_color), - "*** " if line_number == self._selected_line else " ", - upload.room_id, - "\"", - upload.filepath, - "\"", - upload.mimetype, - SCRIPT_NAME, - upload.server_name, - )) - W.prnt_y(self._ptr, (line_number * 2) + 2, first_line) - - status_color = "{},{}".format("green", "blue") - status = "{}{}{}".format( - W.color(status_color), - upload.state.name, - W.color(line_color) - ) - - second_line = ("{color}{prefix} {status} {progressbar} " - "{done} / {total}").format( - color=W.color(line_color), - prefix="*** " if line_number == self._selected_line else " ", - status=status, - progressbar=progress(upload.done, upload.total), - done=W.string_format_size(upload.done), - total=W.string_format_size(upload.total)) - - W.prnt_y(self._ptr, (line_number * 2) + 3, second_line) - - -def find_upload(uuid): - return UPLOADS.get(uuid, None) - - -def handle_child_message(upload, message): - if message["type"] == "progress": - upload.done = message["data"] - - elif message["type"] == "status": - if message["status"] == "started": - upload.state = UploadState.active - upload.total = message["total"] - upload.mimetype = message["mimetype"] - upload.file_name = message["file_name"] - - elif message["status"] == "done": - upload.state = UploadState.finished - upload.content_uri = message["url"] - upload.file_keys = message.get("file_keys", None) - - server = SERVERS.get(upload.server_name, None) - - if not server: - return - - server.room_send_upload(upload) - - elif message["status"] == "error": - upload.state = UploadState.error - - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.render() - - -@utf8_decode -def upload_cb(data, command, return_code, out, err): - upload = find_upload(UUID(data)) - - if not upload: - return W.WEECHAT_RC_OK - - if return_code == W.WEECHAT_HOOK_PROCESS_ERROR: - W.prnt("", "Error with command '%s'" % command) - return W.WEECHAT_RC_OK - - if err != "": - W.prnt("", "Error with command '%s'" % err) - upload.state = UploadState.error - - if out != "": - upload.buffer += out - messages = upload.buffer.split("\n") - upload.buffer = "" - - for m in messages: - try: - message = json.loads(m) - except (JSONDecodeError, TypeError): - upload.buffer += m - continue - - handle_child_message(upload, message) - - return W.WEECHAT_RC_OK diff --git a/weechat/python/matrix/utf.py b/weechat/python/matrix/utf.py deleted file mode 100644 index 4d71987..0000000 --- a/weechat/python/matrix/utf.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2014-2016 Ryan Huber -# Copyright (c) 2015-2016 Tollef Fog Heen - -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: - -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from __future__ import unicode_literals - -import sys - -# pylint: disable=redefined-builtin -from builtins import bytes, str -from functools import wraps - -if sys.version_info.major == 3 and sys.version_info.minor >= 3: - from collections.abc import Iterable, Mapping -else: - from collections import Iterable, Mapping - -# These functions were written by Trygve Aaberge for wee-slack and are under a -# MIT License. -# More info can be found in the wee-slack repository under the commit: -# 5e1c7e593d70972afb9a55f29d13adaf145d0166, the repository can be found at: -# https://github.com/wee-slack/wee-slack - - -class WeechatWrapper(object): - def __init__(self, wrapped_class): - self.wrapped_class = wrapped_class - - # Helper method used to encode/decode method calls. - def wrap_for_utf8(self, method): - def hooked(*args, **kwargs): - result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs)) - # Prevent wrapped_class from becoming unwrapped - if result == self.wrapped_class: - return self - return decode_from_utf8(result) - - return hooked - - # Encode and decode everything sent to/received from weechat. We use the - # unicode type internally in wee-slack, but has to send utf8 to weechat. - def __getattr__(self, attr): - orig_attr = self.wrapped_class.__getattribute__(attr) - if callable(orig_attr): - return self.wrap_for_utf8(orig_attr) - return decode_from_utf8(orig_attr) - - # Ensure all lines sent to weechat specify a prefix. For lines after the - # first, we want to disable the prefix, which is done by specifying a - # space. - def prnt_date_tags(self, buffer, date, tags, message): - message = message.replace("\n", "\n \t") - return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)( - buffer, date, tags, message - ) - - -def utf8_decode(function): - """ - Decode all arguments from byte strings to unicode strings. Use this for - functions called from outside of this script, e.g. callbacks from weechat. - """ - - @wraps(function) - def wrapper(*args, **kwargs): - - # Don't do anything if we're python 3 - if sys.hexversion >= 0x3000000: - return function(*args, **kwargs) - - return function(*decode_from_utf8(args), **decode_from_utf8(kwargs)) - - return wrapper - - -def decode_from_utf8(data): - if isinstance(data, bytes): - return data.decode("utf-8") - if isinstance(data, str): - return data - elif isinstance(data, Mapping): - return type(data)(map(decode_from_utf8, data.items())) - elif isinstance(data, Iterable): - return type(data)(map(decode_from_utf8, data)) - return data - - -def encode_to_utf8(data): - if isinstance(data, str): - return data.encode("utf-8") - if isinstance(data, bytes): - return data - elif isinstance(data, Mapping): - return type(data)(map(encode_to_utf8, data.items())) - elif isinstance(data, Iterable): - return type(data)(map(encode_to_utf8, data)) - return data diff --git a/weechat/python/matrix/utils.py b/weechat/python/matrix/utils.py deleted file mode 100644 index ce5f9d1..0000000 --- a/weechat/python/matrix/utils.py +++ /dev/null @@ -1,207 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals, division - -import time -from typing import Any, Dict, List - -from .globals import W - -if False: - from .server import MatrixServer - - -def key_from_value(dictionary, value): - # type: (Dict[str, Any], Any) -> str - return list(dictionary.keys())[list(dictionary.values()).index(value)] - - -def server_buffer_prnt(server, string): - # type: (MatrixServer, str) -> None - assert server.server_buffer - buffer = server.server_buffer - now = int(time.time()) - W.prnt_date_tags(buffer, now, "", string) - - -def tags_from_line_data(line_data): - # type: (str) -> List[str] - tags_count = W.hdata_get_var_array_size( - W.hdata_get("line_data"), line_data, "tags_array" - ) - - tags = [ - W.hdata_string( - W.hdata_get("line_data"), line_data, "%d|tags_array" % i - ) - for i in range(tags_count) - ] - - return tags - - -def create_server_buffer(server): - # type: (MatrixServer) -> None - buffer_name = "server.{}".format(server.name) - server.server_buffer = W.buffer_new( - buffer_name, "server_buffer_cb", server.name, "", "" - ) - - server_buffer_set_title(server) - W.buffer_set(server.server_buffer, "short_name", server.name) - W.buffer_set(server.server_buffer, "localvar_set_type", "server") - W.buffer_set( - server.server_buffer, "localvar_set_nick", server.config.username - ) - W.buffer_set(server.server_buffer, "localvar_set_server", server.name) - W.buffer_set(server.server_buffer, "localvar_set_channel", server.name) - - server.buffer_merge() - - -def server_buffer_set_title(server): - # type: (MatrixServer) -> None - if server.numeric_address: - ip_string = " ({address})".format(address=server.numeric_address) - else: - ip_string = "" - - title = ("Matrix: {address}:{port}{ip}").format( - address=server.address, port=server.config.port, ip=ip_string - ) - - W.buffer_set(server.server_buffer, "title", title) - - -def server_ts_to_weechat(timestamp): - # type: (float) -> int - date = int(timestamp / 1000) - return date - - -def strip_matrix_server(string): - # type: (str) -> str - return string.rsplit(":", 1)[0] - - -def shorten_sender(sender): - # type: (str) -> str - return strip_matrix_server(sender)[1:] - - -def string_strikethrough(string): - return "".join(["{}\u0336".format(c) for c in string]) - - -def string_color_and_reset(string, color): - """Color string with color, then reset all attributes.""" - - lines = string.split('\n') - lines = ("{}{}{}".format(W.color(color), line, W.color("reset")) - for line in lines) - return "\n".join(lines) - - -def string_color(string, color): - """Color string with color, then reset the color attribute.""" - - lines = string.split('\n') - lines = ("{}{}{}".format(W.color(color), line, W.color("resetcolor")) - for line in lines) - return "\n".join(lines) - - -def color_pair(color_fg, color_bg): - """Make a color pair from a pair of colors.""" - - if color_bg: - return "{},{}".format(color_fg, color_bg) - else: - return color_fg - - -def text_block(text, margin=0): - """ - Pad block of text with whitespace to form a regular block, optionally - adding a margin. - """ - - # add vertical margin - vertical_margin = margin // 2 - text = "{}{}{}".format( - "\n" * vertical_margin, - text, - "\n" * vertical_margin - ) - - lines = text.split("\n") - longest_len = max(len(l) for l in lines) + margin - - # pad block and add horizontal margin - text = "\n".join( - "{pre}{line}{post}".format( - pre=" " * margin, - line=l, - post=" " * (longest_len - len(l))) - for l in lines) - - return text - - -def colored_text_block(text, margin=0, color_pair=""): - """ Like text_block, but also colors it.""" - return string_color_and_reset(text_block(text, margin=margin), color_pair) - -def parse_redact_args(args): - args = args.strip() - - had_example_text = False - - try: - event_id, rest = args.split("|", 1) - had_example_text = True - except ValueError: - try: - event_id, rest = args.split(" ", 1) - except ValueError: - event_id, rest = (args, "") - - if had_example_text: - rest = rest.lstrip() - reason = None # until it has been correctly determined - if rest[0] == '"': - escaped = False - for i in range(1, len(rest)): - if escaped: - escaped = False - elif rest[i] == "\\": - escaped = True - elif rest[i] == '"': - reason = rest[i+1:] - break - else: - reason = rest - - event_id = event_id.strip() - if reason: - reason = reason.strip() - # The reason might be an empty string, set it to None if so - else: - reason = None - - return event_id, reason