From ed3b9c95389d5c7e81008eda5c008059f39c1699 Mon Sep 17 00:00:00 2001 From: Andre Polykanine Date: Mon, 20 Aug 2018 01:15:31 +0300 Subject: [PATCH 01/75] Localize geocoding to the current language --- src/controller/mainController.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controller/mainController.py b/src/controller/mainController.py index e89c5b15..59187af2 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -40,6 +40,7 @@ import logging import webbrowser from mysc import localization import os +import languageHandler log = logging.getLogger("mainController") @@ -952,7 +953,7 @@ class Controller(object): if tweet["coordinates"] != None: x = tweet["coordinates"]["coordinates"][0] y = tweet["coordinates"]["coordinates"][1] - address = geocoder.reverse_geocode(y, x) + address = geocoder.reverse_geocode(y, x, language = languageHandler.curLang) if event == None: output.speak(address[0].__str__().decode("utf-8")) else: self.view.show_address(address[0].__str__().decode("utf-8")) else: @@ -972,7 +973,7 @@ class Controller(object): if tweet["coordinates"] != None: x = tweet["coordinates"]["coordinates"][0] y = tweet["coordinates"]["coordinates"][1] - address = geocoder.reverse_geocode(y, x) + address = geocoder.reverse_geocode(y, x, language = languageHandler.curLang) dlg = commonMessageDialogs.view_geodata(address[0].__str__()) else: output.speak(_(u"There are no coordinates in this tweet")) From 7296d2a939da9f823137cfe9c2463ea99549d85d Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 22 Aug 2018 13:06:08 -0500 Subject: [PATCH 02/75] Extended tweets should be displayed properly in lists --- src/controller/mainController.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 59187af2..d3c028f8 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -1339,7 +1339,7 @@ class Controller(object): buff = self.search_buffer("home_timeline", account) if create == True: if buffer == "favourites": - favourites = buffersController.baseBufferController(self.view.nb, "get_favorites", "favourites", buff.session, buff.session.db["user_name"]) + favourites = buffersController.baseBufferController(self.view.nb, "get_favorites", "favourites", buff.session, buff.session.db["user_name"], tweet_mode="extended") self.buffers.append(favourites) self.view.insert_buffer(favourites.buffer, name=_(u"Likes"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) favourites.start_stream(play_sound=False) @@ -1373,7 +1373,7 @@ class Controller(object): if create in buff.session.settings["other_buffers"]["lists"]: output.speak(_(u"This list is already opened"), True) return - tl = buffersController.listBufferController(self.view.nb, "get_list_statuses", create+"-list", buff.session, buff.session.db["user_name"], bufferType=None, list_id=utils.find_list(create, buff.session.db["lists"])) + tl = buffersController.listBufferController(self.view.nb, "get_list_statuses", create+"-list", buff.session, buff.session.db["user_name"], bufferType=None, list_id=utils.find_list(create, buff.session.db["lists"]), tweet_mode="extended") buff.session.lists.append(tl) pos=self.view.search("lists", buff.session.db["user_name"]) self.insert_buffer(tl, pos) From ad484a1f3517a3965f594691b283df7603324c16 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 22 Aug 2018 17:11:15 -0500 Subject: [PATCH 03/75] Display extended tweets properly after being sent. #253 --- src/controller/buffersController.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/controller/buffersController.py b/src/controller/buffersController.py index 592b635e..390dd3e9 100644 --- a/src/controller/buffersController.py +++ b/src/controller/buffersController.py @@ -162,7 +162,7 @@ class bufferController(object): else: text = twishort.create_tweet(self.session.settings["twitter"]["user_key"], self.session.settings["twitter"]["user_secret"], text, 1) if not hasattr(tweet, "attachments") or len(tweet.attachments) == 0: - item = self.session.api_call(call_name="update_status", status=text, _sound="tweet_send.ogg") + item = self.session.api_call(call_name="update_status", status=text, _sound="tweet_send.ogg", tweet_mode="extended") # else: # call_threaded(self.post_with_media, text=text, attachments=tweet.attachments, _sound="tweet_send.ogg") if item != None: @@ -550,7 +550,7 @@ class baseBufferController(bufferController): if len(users) > 0: config.app["app-settings"]["mention_all"] = message.message.mentionAll.GetValue() config.app.write() - params = {"_sound": "reply_send.ogg", "in_reply_to_status_id": id,} + params = {"_sound": "reply_send.ogg", "in_reply_to_status_id": id, "tweet_mode": "extended"} text = message.message.get_text() if twishort_enabled == False: excluded_ids = message.get_ids() @@ -570,6 +570,7 @@ class baseBufferController(bufferController): else: params["call_name"] = "update_status_with_media" params["media"] = message.file + item = self.session.api_call(**params) if item != None: pub.sendMessage("sent-tweet", data=item, user=self.session.db["user_name"]) @@ -641,7 +642,7 @@ class baseBufferController(bufferController): text = retweet.message.get_text() text = text+" https://twitter.com/{0}/status/{1}".format(tweet["user"]["screen_name"], id) if retweet.image == None: - item = self.session.api_call(call_name="update_status", _sound="retweet_send.ogg", status=text, in_reply_to_status_id=id) + item = self.session.api_call(call_name="update_status", _sound="retweet_send.ogg", status=text, in_reply_to_status_id=id, tweet_mode="extended") if item != None: pub.sendMessage("sent-tweet", data=item, user=self.session.db["user_name"]) else: @@ -649,8 +650,11 @@ class baseBufferController(bufferController): if hasattr(retweet.message, "destroy"): retweet.message.destroy() def _direct_retweet(self, id): - item = self.session.api_call(call_name="retweet", _sound="retweet_send.ogg", id=id) + item = self.session.api_call(call_name="retweet", _sound="retweet_send.ogg", id=id, tweet_mode="extended") if item != None: + # Retweets are returned as non-extended tweets, so let's get the object as extended + # just before sending the event message. See https://github.com/manuelcortez/TWBlue/issues/253 + item = self.session.twitter.show_status(id=item["id"], include_ext_alt_text=True, tweet_mode="extended") pub.sendMessage("sent-tweet", data=item, user=self.session.db["user_name"]) def onFocus(self, *args, **kwargs): @@ -817,7 +821,7 @@ class directMessagesController(baseBufferController): config.app["app-settings"]["longtweet"] = message.message.long_tweet.GetValue() config.app.write() if message.image == None: - item = self.session.api_call(call_name="update_status", _sound="reply_send.ogg", status=message.message.get_text()) + item = self.session.api_call(call_name="update_status", _sound="reply_send.ogg", status=message.message.get_text(), tweet_mode="extended") if item != None: pub.sendMessage("sent-tweet", data=item, user=self.session.db["user_name"]) else: @@ -1022,7 +1026,7 @@ class peopleBufferController(baseBufferController): config.app["app-settings"]["longtweet"] = message.message.long_tweet.GetValue() config.app.write() if message.image == None: - item = self.session.api_call(call_name="update_status", _sound="reply_send.ogg", status=message.message.get_text()) + item = self.session.api_call(call_name="update_status", _sound="reply_send.ogg", status=message.message.get_text(), tweet_mode="extended") if item != None: pub.sendMessage("sent-tweet", data=item, user=self.session.db["user_name"]) else: From 464fce07b036177c89930f1033a79e4fe72f666b Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 26 Aug 2018 02:28:30 -0500 Subject: [PATCH 04/75] Added FreakyBlue as an option for next snapshot. #247 --- src/sounds/FreakyBlue/Image.ogg | Bin 0 -> 11073 bytes src/sounds/FreakyBlue/audio.ogg | Bin 0 -> 6427 bytes src/sounds/FreakyBlue/create_timeline.ogg | Bin 0 -> 18295 bytes src/sounds/FreakyBlue/delete_timeline.ogg | Bin 0 -> 18251 bytes src/sounds/FreakyBlue/dm_received.ogg | Bin 0 -> 12016 bytes src/sounds/FreakyBlue/dm_sent.ogg | Bin 0 -> 11921 bytes src/sounds/FreakyBlue/error.ogg | Bin 0 -> 20599 bytes src/sounds/FreakyBlue/favourite.ogg | Bin 0 -> 27703 bytes .../FreakyBlue/favourites_timeline_updated.ogg | Bin 0 -> 27872 bytes src/sounds/FreakyBlue/geo.ogg | Bin 0 -> 7770 bytes src/sounds/FreakyBlue/limit.ogg | Bin 0 -> 6039 bytes src/sounds/FreakyBlue/list_tweet.ogg | Bin 0 -> 14239 bytes src/sounds/FreakyBlue/max_length.ogg | Bin 0 -> 7217 bytes src/sounds/FreakyBlue/mention_received.ogg | Bin 0 -> 18307 bytes src/sounds/FreakyBlue/new_event.ogg | Bin 0 -> 10206 bytes src/sounds/FreakyBlue/ready.ogg | Bin 0 -> 30574 bytes src/sounds/FreakyBlue/reply_send.ogg | Bin 0 -> 15894 bytes src/sounds/FreakyBlue/retweet_send.ogg | Bin 0 -> 19362 bytes src/sounds/FreakyBlue/search_updated.ogg | Bin 0 -> 5500 bytes src/sounds/FreakyBlue/trends_updated.ogg | Bin 0 -> 13607 bytes src/sounds/FreakyBlue/tweet_received.ogg | Bin 0 -> 10425 bytes src/sounds/FreakyBlue/tweet_send.ogg | Bin 0 -> 10388 bytes src/sounds/FreakyBlue/tweet_timeline.ogg | Bin 0 -> 11118 bytes src/sounds/FreakyBlue/update_followers.ogg | Bin 0 -> 9504 bytes src/sounds/FreakyBlue/volume_changed.ogg | Bin 0 -> 6423 bytes 25 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/sounds/FreakyBlue/Image.ogg create mode 100644 src/sounds/FreakyBlue/audio.ogg create mode 100644 src/sounds/FreakyBlue/create_timeline.ogg create mode 100644 src/sounds/FreakyBlue/delete_timeline.ogg create mode 100644 src/sounds/FreakyBlue/dm_received.ogg create mode 100644 src/sounds/FreakyBlue/dm_sent.ogg create mode 100644 src/sounds/FreakyBlue/error.ogg create mode 100644 src/sounds/FreakyBlue/favourite.ogg create mode 100644 src/sounds/FreakyBlue/favourites_timeline_updated.ogg create mode 100644 src/sounds/FreakyBlue/geo.ogg create mode 100644 src/sounds/FreakyBlue/limit.ogg create mode 100644 src/sounds/FreakyBlue/list_tweet.ogg create mode 100644 src/sounds/FreakyBlue/max_length.ogg create mode 100644 src/sounds/FreakyBlue/mention_received.ogg create mode 100644 src/sounds/FreakyBlue/new_event.ogg create mode 100644 src/sounds/FreakyBlue/ready.ogg create mode 100644 src/sounds/FreakyBlue/reply_send.ogg create mode 100644 src/sounds/FreakyBlue/retweet_send.ogg create mode 100644 src/sounds/FreakyBlue/search_updated.ogg create mode 100644 src/sounds/FreakyBlue/trends_updated.ogg create mode 100644 src/sounds/FreakyBlue/tweet_received.ogg create mode 100644 src/sounds/FreakyBlue/tweet_send.ogg create mode 100644 src/sounds/FreakyBlue/tweet_timeline.ogg create mode 100644 src/sounds/FreakyBlue/update_followers.ogg create mode 100644 src/sounds/FreakyBlue/volume_changed.ogg diff --git a/src/sounds/FreakyBlue/Image.ogg b/src/sounds/FreakyBlue/Image.ogg new file mode 100644 index 0000000000000000000000000000000000000000..81ad0b1d5dec4b9b0ebf2a8c57d53aaada9a3432 GIT binary patch literal 11073 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
Po=J)8X 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/delete_timeline.ogg b/src/sounds/FreakyBlue/delete_timeline.ogg new file mode 100644 index 0000000000000000000000000000000000000000..35443d2d6298ea5c2c929e904b2c2509d7f45a76 GIT binary patch literal 18251 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 literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/dm_received.ogg b/src/sounds/FreakyBlue/dm_received.ogg new file mode 100644 index 0000000000000000000000000000000000000000..4e716ff2c99e0f2240dc31541611d7cb349cdca1 GIT binary patch literal 12016 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 literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/dm_sent.ogg b/src/sounds/FreakyBlue/dm_sent.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e282d8542cf9b32e5995868e0151f382d65d7d10 GIT binary patch literal 11921 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 CYD63W literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/error.ogg b/src/sounds/FreakyBlue/error.ogg new file mode 100644 index 0000000000000000000000000000000000000000..2891921fa7bad8eaf38a674d4199b13afbd82c79 GIT binary patch literal 20599 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

`jsrWUy;T4U_+t2udZiD@_^(CsLM(;z4 zy{2;$dbQ-KO_pe;#ht zMO?k~wLE9cm+IrG=u0=Z#+%;JLmwZF+|-=pH&wznZcX}hp+~1>G5+qZs-<}9y!!`3 zt+P{EG7WsK0d>C$)&qz3sn~HtZoL*HUgY}0$tc^ap5;0B%m-@>8VcuzWYj0NX{*V| zKb=pA?9Gjazyb_{S!X71GQ}*yQd)_>q`}g2ZcD|U-=?UqX4j3F9YeG@XV$sxDmyyX zhQFlsAelnpySIN_M=~^ zvGWO14+Bs-Hy+xXhlpHT;n0Qo3z(QJnxTv*psaW@=~({0(>H?47jkAs1%EQPNIF$@ z;%35qFz;ffoQcVwfMp6I?Rh3)?x_yS*8qX?h3$dAuTy{#Z#3Me`>@6RFIGG|4^zqD zmI#eCgqtEtEw~X}2d;_;S9r+V+A5jCrrwP{jG}+@pxn9=<{6Dd{-iKGVog5F3fOJ* zKrlfN#9UPJW}POE9|ok`q{VQy4b>A7O< z4zxXpibTbR*wRiK6RsC{9wvF^%Mj;2{#(7Eory6nF^Q11C9;MTMeq<>Lbww{ytqk&z@?55FUJ;&5<~Z80uhMBPHL_O z`I~7MIS$wr36R*~4^{gF0vouYzLH5Lr;H@a<-YSH4Rn%YqXf(ceaq*{+^BH6`by}y zZolLjdRScuZiYM_F>!fHDiWi0b0ihzeJ-|gWWJmglg7#V<;Bf}U^P*>0DYp3~lfJy@_{n{|c9e#$azi;`H zCzQ7oSOx$>iCb7;rnnLRb>X`i+Uu75Pd#HNR^@cPa*gcFR0i)6z}e@ zTe>(MSiuh&vqd0Em{=r-Wl4v;oT9K%^~1~zWUX-=e)k>F&UQCphCzRNimyrI9NqGp zoA2`*_K&nLVOQm1c~mn{yNzDz>fW_xN}d;@)CHC&%ydbs+0dcj-P)`8YP?5|tB-7- zbN1;tQu#{W+Og(TtnTg|2A-YmuU$IcXu{_@e18tw>7XaS+KjK#O@!wIpod4#yzL#R zffFqjWqf30OUdWgR%WrI6dD^fXjd zz}1SBD}aPW+JhO(gcTaeTlP?B>6cC?$-rxR@HBVtk4Pm7khR47`40HOa%#-5{`wWI zmBiJu$-$s}^&?unscd(vUo7DkBsyV&Lt_;=j1Tns7{mL#cPo1RL{F3;dGHK~zT+=Kqtw%wu!Upju+tEAP#yPVw}^9)}c<&21k$o0)7gqxFOAIqi)F z_QmPDozU&_CLBI-gNycdEFOO|Bl7iw%vhSzf^@4*P3SaL#l($RoS1LC*i_q1e-gpE zn4}%lN4#!1wxhh=)N~-lcli8LhMz)4)vcoJ8~V(e92WUphpDj#yP#)^q;s#`b!{xv z|C?2tFMm>QjYoo{Kx!+XfX;WZ)<9As5#X1FcbU;E$8f1Yu{qO1`5B z-;Agx+0&2 z%?G0A`%a0$g1@ddu5_(h=H~if1WW3uA=z+oS1C8NIKj>fGvo`GXVtDa+mZq~&2oKi z94N4KKKnP(pwR?~SFgg&c6Y~03Am3<#U(qcSsN2oKAO5my?kBOECJ*0z!xPAcrL5pocdzd@2Ze0%~9Ya&Y)dPs9K%PrjKIp1|B{E&DFmu{eBK(frw4;`Dd9O z2E&Cnceq!@4(=k*M2g)a7d*NMxBK-vk2h`3Gv2O#6!NUHui)!6Zu3a!zaYiQNL`qn zqNZNI3beL`DFn%SGw%Yw3DeE}yU;aUOREL#y;Q7bezdJ2cMp-1sNMvvGPt(nKTN|< zd?V!Cep@2?!siLr@AVep6;m+NQF9PV+QD-#)|i)6Ip^!`xkI}c$uVw7hAf#GIB}zDspY^XWN=v_ zQ^C91K`Q5y!fn;pNyi%@7j?pR1K*AWCMW4m=6?+bw8YkxpS4xv3)+13E-0Fx$S5i8 zy$=4dmfJ8=y3;aAx<9Y?>Up7G`A%)`1W6_}bzkvxJ8q{iw3l(eC^Y2an&}H^vYiZ* zZ@FeAoG>Ds!_9VA@QIuloxXOR}eFO=iPV=qyLLh*$T%hL%&9=lD+cH3=a+0h|yi zCh7Mrc0lvy(rR!-QJ}9I4QYEgQU2wZQV_o&3Ucd{H1?%XPK|3uDTyf3k8B-0O z7e5o`BnovnxMgq9LA+VH^U? z;<6HvYEKiHk%^#~Ol?E`Oi7DlofK=fLr%gR+261r{;hrJoTi05u z3e1+t&5Px$gXIIe&AAy_|IvD)y)-?yu;wq3kar!;lHS8JQ?_2=1J}q?LuFUX@I10} zHc`?cyBCe(pI=C$a5Mbiyh`-OjqpqvsM;e7OWP(UBzZCZQT=5Z#NLIP;JbOSfrB>7 zQ&prTUE{3)!A}NNlHd%iyb&LBkg7QiaAOEiOUtlP&;rGTV5bjR@y|eaWYfEQz$Cp1 zQzN-ik5A?vkYMOrlHZLFbVgn+etMM@cpB8!_+pk@ZbW!^=(9?=g*M2wB?tu8otMbE z5O#5N^&-A*8FyUUKHSSPq2(Pq5WB)Udo%#uBQn;Kq`p%M@}G3`r7u-&Y8fnXZZT@Z zxwgy;NPMndY|x8UJ#@{&yG(TX$$=?z*tM^NVz)RruvPQ8LCm$vx2UQy?YeG6r@0z( zcc83LOJh5~rg*+&Yxh*cODCaUxXkZnol%=woF4ag@Dzi4`_^V;&ux<0+pR^YwFz5h zxMvTcZ?@U>lY_PF8zeM}7JHu`TtBqih6Zs@Q6cqT8S6~(DfGRV3GuRZ*Fx7gRhw@gi^4_9tH=sRx`5g}tYJgNPzVYaIn(8M&+kizwik*J~(1a&Xg$kf+b) z05&ebmAmsB=)fG;_1&{A5)7c7%=RU98qc?%;3;-TtEy0NI6**T;|;V!P}3c6Z6|Qq z3z(T^{T%G~^Y=p!!E(o(;cq-q2^QM?KGscvBHP#M&!Fu)eobkg=O)OAx_yLcy&hYd zP}0rMM%G;74kfvyOJ4TqGA|4=ymHQhw?|i}UBQf)CjB;siLLM%m#g3>B1Wz!1Eqy9 zcQ?1hXV(?$)!S=y#XP%54338a0dAE$(CG`|o>eUo6&~r^Y^a!MN?V3twKl0^U1>R< z=?U+(`^4l>l}?jenEa-ZDz>c2iQCr;gG08MOb zQ@4Mz6ZqwigmH?amVo)i)HstjE{P%5Jfus_T&I|h+8OCI6FEc)`L4gvw*M(U7s#;Z z`$f%T&}`Z2@Er_3u8=->@q`nKQGMljvIB8ZJwur$J0^P)XZdgtaXG`_YqE{3h6FD8 zx~%Oi+lqP|>TyhD{gg;^zB{!#mB_-eXx^r&hr+TLxuMDjmrdwZVO-v|bRImy_UYe9 z1Kdj5lubJ5b2>7`iAP8G&rYQt&v8%s<#1)0p8d}UQKyd9J*QUIrZs;zo`_-kU3bE9afXApL~+dkY(Ww*H?Lisd% zo0P=m)K11!baYf~<<|M&<+Sh1rCrULqOud?A!L+U`lDj8xhpDW!ZGdqq=zPvAN6gd zU29VF#60KTs*ur0eYxSTmhQ(MO1NHiK@I{+LNp|KkC8POS*W84Pf0riBcg@AQ-5m0 zo2k4+ozW0%N=Z*OS?k=2$V|+r$Ir%2MG2GweJu^M6>+EN=cB~tDKE3{VzJ8iS-I=W zd#!V2+5eOG?X$qk9z7S>GY7mnzOqjjD4VNKZkjHfl zbN2G*iJ0~!SS=J@ze;m*I=o&u&5_<7ClwfE{?tdl6p?<#>C>t;b8&HXX_Dh5Y<7CC zpZgRV+;$Ty8*ZcQLRo#8Fp6B8ekgHy5tJ;ZO3kXLn%5e|zwT-}I&@RvS-&HRuHO3_ zu5+TXforbAK_#P7e-$95y5TRft8b@U-L?wdyl7~t-ErCy9H>*2aR*EXau|DdmmnR= zzYfJK+mt7N-kMzW?taU{Gfb?6PnN3ALS>1JTS#70`_-ay7b!b`zGaI6wglT8MLHv3zVewVjAuJqIjA= zQCQ~2?Q$5c+AYQqp$ zQfq%1{gWdE@5iR?9;xKXOjKX(Nu4b`NEj|9fW1^PVr2UH@*rfBCwTK^8C-A0Zp4N% z6S=}oCL-9s^ z8-u)=_0!_3v(v%rwbA*}+p}*sa~YPfgOfF{*S;R(0YzDDF+Q6yHQpP#ZyTL$gJ3ym z)^BAEu(2Pz6>u;>Ybz+rup%Vz%|*XtWzDy>#And^1$b_$&qOF< z;oH66#I8cJ38CyiVVS=U0Nyo>5bas?wE@qzSHW?n+2_T|5^3hYM+1#Wb{q{Cvq9y zw|WwCZZ5a2X;dWc-nLW8azh2Q;`&m4ERykTrj`=+XlGXP-|H>7$1rdwxK{ui(>~*( zwN`18$D+=<$|^PSO>w)v;>;YHSVkXytjC%~z48IP>rq&oD|YNB`5d3%5}{QKGouqw7u- zd{@4_+sfMfV%Fk%TU}&jynyNrW4kV3(&ymIJ6nn5eObo9&HY_vJJn;|+T?{XK8l0g z^7D!H)p-}&l1!?BlRz~CBHOLoxHIG7E#lJ&*JnCYBN-qX=s*F0mH+NN_5t?d7|tS) zLCOG5637P~v@pu3>ZIDUtg^+MQMsU#(Y(0AEOGSAIr5mmCt*@*mKD+IM9N>~*<=Dd z@@^T}EAiuf?Z7I)Iaa@9=FQYcHqS*I3e&!YU6|bCNcI3c{--1~X0qz{|I3{_(K zkzHT*v5s{F->$+iC|I*=Y`s`j9eF57Np--DJ&u@=ay6I1yU<0eh70ZJwO#p&zFV%9 zCr0pDyBU~^lRwi`56?`a3;cR@d9iaYr?RFiH4{5J!}sd^_)YqwaQ^z(vthdH4^H%c zS}QtSZEiWcTiMMgWG z&a(m>pDm8jiOMPzYsdL+HT@II^?ftm4AGa;PA&5K@t*yLa7zupX6HRR#a-9_{VJ-^ z)jJW?2{EGMw6Hpv!^%aAU^F?jK~L-ElF>EQ!=MQWc?&9*!ZO#VAu6Ae#Q3J2>rT2s z0G=TcpxL+8FXoB?tdZhBfTYkQdiPu<2S4B4cE+oekm6^wn8K?DOiO7W3V(RQ|5QE$ zDntbWV8%^oq9oypQ5Ga}Yye^3a<5|hbnsltVTxoVBaoZW<`NgdaUt#Tb;6)y7L#o8 zrR1nqnpbxl?4{XS#Qx)%2y)AfO6;)Q_XJK_+e$0O{k~MyyC;wuGHnl0?;!v}3eM#| z{Gb5R=n&}{cN&+F%+R})P^#9W0t@^icyaKN&g>2vQ?Yxh zuuKO{o18R>Csr#BPtYZFhXCQ6=E)C8@ph(Fxp2jZQD3XNV%v+=y?<0e|3?4E1AVmY zRbrdG$NDc`A4y?7!@!M4O9!ZWl}}1Oh7!>?Jo;Ef(tGMwnM^nTwG6FB8 z?oDxEQTB;p3L;qb{yi3;kdl@nQ6aBmyR~;iTkHJmlBxhlzqooS;y^q-t*!0mFnDL_ z{AsRO0E~0PeQimj(mAc6`ei;^9}i|!P=LFKJE#fWwqywo3uqgvlr`(J^%IxPDX`ti z6LMT!n7Q)PeiU2epLwTtcHmt8q=bWg3BP4DdTlA7e7(*w^GsBaJBhrKdGnE3cln^q z*^>W7U}SQWofD%?{bBXerz^e|kY`>)nUb*z^YQ`5KEc#)>^|lRe`8NGM%Ey2ZmE+& zHg%07e)FH3MLJkbibs#2)ey#QYq{|6LV+iJ?KGx6q>^CIHxyeQc{P?r^))V+t*f7< zZP`&LaKVW#4#!b%rTwGj@Be z%-`r#KSr1LmKyL>*6xCSn%$h+z2`3;iXHf*X-Og{#|?nBMbqzkS!c%SZzJwBH}R&u zU0&a=@8!ACzJRrJ?5v%)d#8*NZkammyq z=J!T+nk6?3Qz8*iIT=-DSmd^G@;bQfoZ_g+V0XtQ0lfkhStstAw&h*X6#fD2 zFR4!xReI`m>{}BsYJgh>qL;Q6BJAZ{lBuSPp^V$J#?gn7)Rlu!k3kTO*Kres*g zk8$Ob{`^Nh36YpMiVUX|F7Up)|3#4@Ch3YF3My5vbN8L{H=6a3{34-V=kh{v7&;N1x3AJg-H@NSFhzj6)O^qi&~Eftj8ZOx2v5; z>(hq(YRFz)e{Q}6GYMffRZV1|PQ!b7`J%m}I)a>3(vJk%s0Ocj*lBCrH# zdztA3+A#Ed_?6P{5ll3tMD5i^-I3fPPuXcSja}>c5Mza+42tIJUhNf$3+)c4n`?YtHzSBylnaY{{KoZ$v=G$ZE7l!UO{0EI%HsG# z(3Z9+tSrizuI#akDPB1qZqee>JIbMwvTnjIE}Tyo0LGly-PM8#UMr)d5cR&~m*m&| z>P2MJYGTI)K?3J8HB)n!4EVcQ$7nlH_n_D#-BB{Iw29pze6N4>?8>e=(D?i`%O z{#8pk^Gsqs?M#0{|LdiTAFZHr*oJ0HhG`%Av2l;y> ziY_3dnI}j=1ujyTkGLLLsNid zFS~8ndL=17lZzoqe!^TDR$LWck+WA~;oppJyUB-N9xN}WBLCJ*=3uWmqgx9SpXJ|* zc#DrvxaUT#8mD8CKfLWOAx}w9`mt;gHl49A?Pk?p1(Z2`$lZRJ>(O$=p2Lq8PK(@; z{cyWcJ|s{lsIedBeHg(#RIzxqJ>R(A0l*QcskhZVeN}M! z?kH>7GzMOcHhq?l|&DO{&V9ON8F*Ksd52nd72p|Y3QE(?=sY04Q&%h_HOT8a|i6RyO8Lu zZI`;|tJsjk+tQwm6(Q&@D>jL_mUyJ2r}rC3hbqMLaj`iQkN?S7vD40w)$l2;wB4pJ zv7|~=a?(_o0bzNMafJ#-NF>M9Q+k^{-_uRX3E3wygIVO92`ICD0A- z!$T*T6CYoUYQgPnS$^*E+ffnzL|9SCH>AXyD+Y(}2bwl&9o$h)-}+(8zN{3~z>+Y< zVLYI1Jtvz^JoYQ!5nC3#2EI|zq99AP#6kSt*qhC9aX3r69rnnvzD0n^Iu>!QswW}N z{h-E+JW1FA0)(bN$CVvu_vl)Z+(&JJC$B*1&)4iX%!3QfV)c)cW@ttNS7J?NV2%AY zT~XiWg8L#fJuZW$t0hI)ll`}Tx=7?mB1u-9L#!TGv{s`k3dk;XRHUJJomtkB<(7)& zHOO<>nP8PAZu_(3dQSM7{A!Ep5E<4U4kr2fj=noWZN`W$oJzYz)b$#TM$~C!ny~jF z8lTq0DgKv|o>6x{A26&>1JEd|Uw6%<1YPun`9Di2o z&SEC$Pp3fOn6&&83^Pzz1I z`KGTRkCeOWuQPpTEp=)JMwMqSY(y55tbgi^bo;1SLh#25Dk$-7_wkg!5f#M0j{xG9nH`7`(GxbrM+nXX=YliC z>EPsWpeKxKsaGsc^djY^b03eoUYfElF3r9C0f9@D2CMNoy%6yt=u=slTwym?R007l z(COzN{M`grz0Pr(aslEL{-9hh<3#zb*y-S?EBJT>p7K5J*8hCQ3?lceEhw+$1;d5! zO{GDK@)V6iI-C6tOVDn_1Z-hBbKo^m*BI3GBejIb27=P-bB1$Qw?>~VEl8yGdd`fwwpBDORbU(I zEh?2kJyzGPAsDa1ZDj=7%fV$RQ%yz=pp8*A%LAe z=UuhO2L4aZKvxPi0P6*h%0*;^?FxgUh<#6irEz644Kjp-{R6r9h)zUdX>J)Pjg@?K2Iw0CnWvaNTP`s>pzNoJ@z zI?hVY-CW9uy9Eg7JTBUIP4q7A@bsN&UF7HV3Cv$ho%r1_Dsiz+=S37$3zLM;&rc-y zI~I@JAYA5Sy}QkT3VK9{L^h$d&fP4Jqr-?gGFFggXISD_7V{4^32)osjPh0}pK?-0 zkMY2>o6T)S^I58PlpSyHpGoE>#o}Z2s0V4bclG3)6UbR8WCxlrGmN-e+)8W> z?Y0!L^Urr1o?f+d8uVDpn0!eJeS(}`pZD2An3;P-f4QP?*P{cZSpfSK1$52-7)G5v zeIhlc@Tu!X2uh03*D&B^mK;8yFgd3nUOpjdZ3K190q+xMFIdR^qLWw~adC28t?oDK(g zz;4A`b9%69HX3qX-qPgaMy`pkmrYrGzSN~NRf$rfw|2-lx3jx{-h3fBM<@dlz6pA` zCt*Mcc|#LN)CRSUx~%e21%R!Mz720C zHfjpn?5yx_vmNe@##Rn{a^o)Iunx(fGe|MN;FglOkL&9u4W>v1N%m!z5K$dIWZ|~* z>t9`mc+YGO?GmYi{Umv@`K0i54D{6Qew^}N^C-K zdk)<(DYN;ZOA&aFSfqJt;^<;i9S!Gt$U*Vt?B(}QE9=S5$~&LvKxyFY}zz; zF~BzrD|R|v6f=@~O)WJaYh9@~y4x5^_;r!L;>R^oH#e)bz;zo5hH9b-872$$-%#)VR{#VE`}g8$8_SL=CRJh>hG7^+3;+NW zLeKj$W%ha&4Ip3SFmq%Z}|M<+4-#jVhu}G@Uc=+6$N2~c3 zjA^bX5z|AJ+oKmejq~wu=mL^VTG>xlP2=b* z6f@K`pyAm${eihTlTGIC-lwXW|Le8XJ>P1}q&RbBme8<&qvL}CN~p_~)!FRn%DM_M zz~1L;1A4me&rMk#5t+|GMYPwBD2P|4W@KsWl;1h{pBQ8e%pJ_^`lGC6Dp!iB(f%R3gATu-PNtcnHwML_#pCl%fvl0bB1CLWUFu0A9%4{Ba5}^SS}g zh^taF6f*x_P60ZP2N63mGv3q&H*QBSP@Q=`6j*GtI8{{)%OsXz7zF?T2=pFuc+<4& zPEW2@^SHTfzVEG#SC8Ioqo$wMpDr~lZuPo5GD1$l8dkmh<>m7fT64F1=8FgFRQdj% z2jDoWU>0!xe5ozv&@mtB;q<9HyT4A(Fcfpq5D)BThOW|2dFJ_mob{7Fskw7#k|T1H z_3MW2K2>$FI5(W65}p~jD`}Xkm@_$;`X6e9LG*r_8FqqkHqL6Y;fAjHLl;eEBfiZ1 zw7!Jj`G}AHi7bBn{{$4is|Km+Dr{z3sUKpmlPVXp^eEW)P(XlBL6uU-E6^liuquDS z0iY9r6}<`KaF7VZx`4P)_DxWKK~!mB!MNZD;2eMgp2+;4?N)$y{MMacns+3kh)+!+ zk@?T_5a4uwXb(Ii6(7UY3=eLwa;^RM^WJbVk4Dcj#U_Sj8Ciy58AbpA0J?7O)Gxd9 zl_1+$8?Bky^JVs9o4+>ybko1ku~&c>{!F}-nGB;^mP4G zht;$q##zNm<=g|N`pJIIx~X|{hPol%*PfPhnY(@dU+WpGV0{eFu0x^W*T87TxxIdj zm-FeIRhzbclAhOxzC$ezL$S8#DX2B?8l-Psc8{f}$N?S-SiOt+Y*}?LhTn|H%yMsh z?~%Fm9A1=dB5`lON>fd}q}8yD?8OR^Gnwoq(_)c~#Ik1zZ^Z@r`WtX}q}~20@A9 zUMZf~N({>+hG7^60{{SkJNS09Em5duXZJsj|7n|W`$dsO-u{gAV^b`=uRpLjobXdW zy}phqC?z%Lg7) zZ)T{050lATHQI9f?DbZS;p^BaX7?uFg<|iXLs4s2b!PUas=L@wQ!gY9h4xJ!wwp{t z`wydbTC0w~_^ZRb3CR}vn_3g21!C7BqOmWq$UNQ?Hvh~*IR4{Jgh6No^9XvyptA4L z(E&>d>$Z_pjRsy$d1Jyvv#vtyIR@Ms3L{#vU+o8!0pHVmykKNwl zTl?$IErl|7prJo7bX)rLeV1<^HZpQO_fi>G3G-jB#d>*OuIv4L`FzjqN$*yeeUR}!jG}85O(U= zst0!#6oo*m*bga@*&i>*Pg_(5I8uT~SpaERM(X`}16K&dBmG#~6v9Id4Fb-Q7yv&? zzJZ=VplO3fQlM59000f1$ozkfZnFUIZ4cdrZzRez#xXaa7XfnBLkC}hXuSEmDB*5& z_(NuRZLtr%cktC)-9BTYUR(rv6gWz4w<_!)A*4 z?$I_Detgb2asRvA=2mAtmH0{;X7+A+8D8N;WL4JZ8xOP$Fu=Y^=C^^V?o60FyLZ>j z`m^S|vd;CH7b<7kX3zR^gLWMtr%rZU-S;fk`=5q_Jv(6>!Oot7Pd`~j(v|ZAYHIJg zs`F$~pR{`or{fG4Lsj9|0npnH=&R00*4_51L;(uGgTw`30cZwL_5fyp02Yszy_5kS z7%&b65U>CM7XV(z{D1oF0tEE{z7`tmnE(G=1!(lKv3;R&3GaEQJ8V`}C5B-D*+>8Y z0001hvg=-(#^J&Lq}7*ie$sD%5PY)-nH|eO3Wd#)B;Qs-v;bERz^yBLUd}000000000`mtQA9Uf+K0 z)2zqupPIk-1%U%a)>Y)$l~htqC%)Z3eoYFh7RA5+*5Pv?y7x2LpmiCb>gc0m(=rq^ z3&>k@Rk2S3s%j2)06bwRs_rQULs5Zc0e}hs?vMHZ+RB#o~nEPx^<_9*_oc6?x$_0pV>vt#zq712=Gr~)c@Pz%Sw$!qD1m^aWl6A zJ%AnsxBY{1hV-Y?jHLFk@;}PM%7>YbR0zp)@%#T$M3DcI;y_?XX$Vs*n>kxpxv98V zzSPi=JCxvZQu#rZ#iY^w6d@;CjW$`HF{&$B5~H1JdKa%bBF4wUGd506;LU zGVzb#pC7aU02~0|V-r9 z04RW9CbTTKRjJ*MOG=VWTy;|B{)b*z)T~ny`O0OvM@I(M%;7=0i-Sb zM{jw@+xR5?X|qc0vP%BDRl;_F86bhsWxp%=-zw)ukNQW$0|10gC4lt(Zme=3o_>&s zoV!fFyYgU)=3s;2vws-@VRl53z+;uw5a~}Af#C5%CU{2iRMdG?CjJ{$1OMG0e?bUB zlWU!3AB@KR!R(J(f)QkiKcPuu{N;o|i2Srpo%J&dIx2{jwehhWKYQX^lx8+#OL;~% z2rRgj;|L~M%K?FzhBA9UX8z0qffX>aivRX}o@b+0myaW0BwIOHeI#ZGJiGBM)bcr9 z#9F<;FSSZ-T!NE28GJ9a_+h5WQ>5oWoJ{2N;6y zKMndH_mM!@&1{yqFZVrlK>QPPe3}xPWJ>o0tmKcJ158Dfaq+Je%R}A(Nf?XW-~W-E z8UP5z`FAg-2`4s>q$$mDd&*o^loZW8^s;`0Ynb(+sp?S1aSk;dTsIDNzpZ>eyj_Dz|6l4EZ zAt&SrXa4KYf1FuM$J0qK-D9CC%xKnQWhP){CeDN?+)6L)Kh?jYBP0PlgwvmJOfiq; z?9T~Nl|uh(;C~`V&K18uk$fmX6NFY8d`1(z zRufuQQ=V3HjaCLt0eX!AE0_OPXFUErau8udD;U2f|DPi#{c>1F>@ zk${T)GyXsNpjScWfjrcIs2~`r;UgjH-*NY;NH?BnH=gYO5tDxwlm8c92#FAkAb9`q zLP&&Q^gqPwf8u%nAC3R3HGptCBrL=qq0}cDmPf$?NPq}>$s}rppFF{qdOvw=NtN-a z^$2+|vz|=+J>DO*A#sYB;K3|F`@n;p^+YA)<$olH0BSqLMAj2^6#0jV63KtX5MtF& zWxN73^-#MECeOhr8?f;t0e;@i^7$OC~1OOrs4BwIuKZ>`|lSpEHW~v7mMYbiCz(;yOe)RBk zWIPOns4sae82~ts0jqEDhsSx;*9Fpw?Ft4GS(PSa60PjAwlZJvj6#)h3$}bp&>>KS z+;-6g6^{_d3qtLWAJoogtH_G=#&$pvKur(m{w0vM;Zv~fQ(&7_G33fR43$yGSJBYY zN?o%}+lnf9nN?Cz!Hi%8`Ex{RX&KpO{R3`g7lHhPZA6r}~q z52Kk>E4?SHh}oeptO!GNBLS$p(S!!ezMzq*G#>--Nx#qnA`!k2Yo^FqjA8Ly>ggvl zWhr?YOCXS$sracBGeas+Qogtd$fd|uoCbVMlbWPhPF(~9GCc+aQwSjbbkV}Xt55)^ zh-}Z|;n3cqmr~^1%mF>8DJDi_Hs$yh4>?Xs5+@?jQB~Z@K)@J_Ek_WrWT4PP@}t01 z?2bS|4FACXM;mPbwPzS|w`wS6C0!1F++#x0FV6vJs0fQn zMsjY`Bl5X09E+a{^2H6rETP09Zxj`)8!|+;$U}kp_H2FYf?L!@llrloFJ=&u6sMWT z)HM@IF*7wxn^z4IrvmNS8i^eaddpH2l{ou|9bWZqJ|Iu)$8ZWYJy(k5?9&g$?dv79 zsA`y@jm7=aJFZ{d@WdjvuBBntezb*{F1CiPLEB;UF(5-k03g9IE6}4juJ_CKEn=5I z3SLCKgdv>7;=vaXCPym=#tI@H4DsUhDFq1p<2{IM@QoBxaZxPi=AVg&{M3%gYDDm1 z0+Fevj05*_K!_E85?6-eNrXIqCxD8IYzR^>5u_gQ5bZxJ5X17~lz&zrmLbMrI0znp z@QPCYl>;M&3TzE2z?Xj~Qj+u$Wd4vsGzdmO#eXJ%ib@E24?OU{NsC; z002f|3Q&Pf_C0MeaZ#ES0|Dp_F#0733s_Yy1*YDlPmSgbO$pKDLQMsZNIs>pQf7Km zok}d!mr(1F^?S0s!wmprbtf*e&q{Ku%3Bj|2#N9FB!cD-rt$dF}W3696?@ zA_*;S9Wnh2O=Q_#cfjZ7?hn`i5>hfdBBGyG!OvQo_ax9H(WG!qTo&cy2>ETlxT-;G|xrlyL z`N#n@g{VPvMF4;rv0Wny8Z`h3=}&_7XN>gEy+knipIN}ey~M)_mKr!GHp#?rc*MH)INzr zMp5`gJdC3?7D0w0Ys$iQtnGR$lNqZ_&-EeY)zEYR>wSsVAok0b0m?DD1}xDk7%lrV z_qD}P?XO5E3V3Z*81<=32d?p-*Ijb(3zt{S2b5WpnKHQ+AdEn5pv#-Ts3K|NkNt>_kK*Ut<^#?2!#2LV^I8dXuihBAyDMNX8f`c zXk&tME3e``uvgx9_>TQ}rwH0a=rCR9m3z=1$ zkQ*-mgJ(GlPP9AkVYipVM-GJNfh(8Ng+l&8&(Q0<39PNjZe=F?-*Be8!3E@YtwnsF z5-p33npq0$Urvl{Tuk-f!#NB_${47Q?gj(`zAR?*E+jbeD~UUaAKhEHx#_beeVm&i zI9@+D%o6kdSp(H{VhM0aMq3uaQBF5@ zuoCA0H5}!*Wi-M%dY@3z(*JoC0gfx4jDv=Ue{j?0*wpppS--?8fLyfA9wqhl9lcx? zqmSsB=k0qvjNLwT8P15JR9}IXchSSoUOch$I)4g-A(;5hM?E5a{QvB zLW{dpJNqCUz-GY}>HX|#vmQ?)eiir07{1A4yfm7K<_1E8WxEf_kG1h0k7>9OGiJAUXJ*q?9@s6?DBTl*bfwKmFw>^&{Wj{B% zdwcj|;BfmnsKdK#e-ynFF6FZM@z(az`f}H0rGt=+Ua*b#hxlly_^^=`Bba^)hJ+=% zGJiSd04R|bw$;n53pn-gw8}X@>sin{stY3Zl(VqPaq?XzG+Suk;N9bWdz_{)I)7R1 za|zrsfZiAvsReCe#}m%NTy%-gETEFcm1Y1G(aZ17x7VyODSrBNK}x$6rovmR1g1eo zU7yc$nos0N!|xbyc(DhTX-VwAwEi|Ykt1snnI}jWfF>HLvCxZe?R7!s^v;iVu8NJ@ zrRTJ3A8`%j@Lwj?v|3C>&h0(cCw-p3$MND^QN>N+$Krvk-IZ;;nwZzg-NmRj>&a<= zzezciZ0~sRQ{=Rm-^)M%U~Q0$3`DYub|qMIBHQ&HV2#L0OrQ5~#YW?v+U#D=EqMIi z3yDuM8l~HMY@^KBEawLp|CX6wzw&M$+Fp!ddjVX861CZ)bKKxVgxD+}#opJ}--s_9 z%4y0}=K}LK6Z@{0PagY6R~TAzZIz)$#fQdN^J{B5iNregN4J@1g_4^87*=pdqAXw! zZ5!Sgn4&dKag9sPk*p`g&}Xdt+&pGloSj*sULzOkdg)z$`c9#WXI=kWbJA39Nj7jv zsHiVFS}C`-%t3U7+FU;5>my>MB#Eaxt_oVakBt4RtZ8Uy38Za{UPn=el7F=0C)}%G zk&oN>Xl_GC^(2oeBx@OU{gH~32VUr1jcWT9Rj^#))SDTz?k#_x030W@$4?2mAHCYP z>qb(RHEYYm2xBFdhR}CXEPcX7=K^&51fRn2JVkVavxbuMgYMv6{(fL8ldik9Va}Sb z33AT9M?@TlH`pA_t?-*mlOVV;Hc{h=-YX#S%F3C>P|w{moBQOMY-D7lQWOCgdT=Bn z!k-)S?rvc;wPmNp@9WNNa&_*h9HUyl{@%uo#V>otsqvFF>DvbRQxCSBOXZDbFqyr0 z*{Y6QLfvHO4sQQ7X#A-A3xlB`S=((DG10gDEP@vFT$f)It8YH64GDC*KQq7+rPpCezCZrAAoD|RH~RVrm1QSskr zm&S774IY&(`dvdXtV)Hc{3*)n=O8-?9_M%dK~=K@T~B40(n*Xm=u1-C44YHuey;*z z^*H5qB*ylBSQ}{b_5KKi$u&>;GfB6O{&v$4b-5Zx@4mk^mhpImy7TmrrD&yOPP)_x zyEXrKDWngK{3bJ*E<~lfo6u44?6RY+Le6+5F-z{&I6dgCy@%GDJ0N9iSFf7COA~oO zpZ6<*ADAESvuv(8kTnI{%vujD3}(JmZu^U~;T!=RoJfA~1`8t3ddC~iHyW0w*B+)d z`T?VtBfvNpt`RZ&i*#2v`28r!K3|Vk+tRS}9h^H4?C-pnqGog_(L{LDd3}udF3YUa zNq0Bo^H4u(vOpq(@Pj#{+L!3v-#$sSJ9xY2*!=~zmw9!5)6>==1~-Hym>fm8!Bs{R z1-yyo?(3~~W}*)6gXm3G_{L;7ywwsfSxtJSfSg_q}% z>&q%(v7lM=H=z@5Uu!Vape77*#VdiluG|_<*IB=Ip1+{TWymd-pFpA)^jc&9Wp$^g zsTK02=N@2vZfV&d#q54Y5kkmR2~pR0cDpzJb4uG0Q@h%-%TI2?&10E#CMT@u(sS9e z$N^kcWjHuC)6!eYvmKUN)%yOR@aL%M`5oryV;)W-z2;@RCE_Cgo;Y|seZduSmHOH) z8NIe}ef`O_zn2Y~M*uv)WYXE8F^=7^!I5pbTwDWG&~{{97!e{1H`aHS~91 zghE$gv($ZabW5Ln8T1+6;{jv@(?!L%vUy^XTnaf^(s!{xlR|o%SWFw2aIfpLd!wq` z*kQHfAeaKILyTk6M{G~%S@ElBOu5Grb3{bZP(R>Uk08nuab7Y*-nRGvSiK%Llgmdo zKYa7xW@N`pHk}pPUjgYl6QdM+g`2E*R3j3Y&?Ro`Ib#?@mOkON+5Ly>?ZuT-jvJq( zYKHcb(m)j0C1Tg+J2?-C#o%}33S424X!M~+Z%{jTObG_F*0*vU!e&ekfExT{&86wADA>7>}T%x@Z_B1w28!{mkB z;mqR|wn*?^isTpe&kS<2pI+kKu`8C)zC(MQHY|St6X7TgZ~ICqxam%t&cdoYU;j6G zAZm@=6GK5^-Zn6|*$bx2)D)Px+a16md&eL{gwAbqdS``iN08kB*31AqEX_4+1j7${ zLB~lFH|JMZ7c*+p9=H+1sT4**%e}js8RG&sWXim9+5$)J^%P9`ZdRj!(sQg=6@{$N z3#TA{nxgvF5fNh>|O518k~&BEf`5k9FHw2dx+s&t*4tV%05>N9O`ty%gcOSP< zdN(WUb+fa6jbyd zRafW@A+iupYx{k-ZQZs+B^6N`nD9&>zW*R$(Bxj~7t0;AqVSmkKBrgQLgIn@Gr^02 zBNK()L%`yAehc>xA_>kdzVVp)@az5zmv7s~EpL^tTXi=lxO275CXOPU#L41vx|Xlb zz0yv1-|elrKxb=OF+kGEM>aLJLQh$lh%yc?38$=>ym0)w2z!LPD}Mh5cEc7R=+9WG z^y8F&ut^`7gkvT;$@COr)3q`GoLNxk5kDE_X4>l`O2^T=qVOF2EGj&TjBro#3eZI9 z8s3mKZr$-O>~2Ci;?VcqHdt>TulTAQv;OG(p-OSSv;XpukE`p<(F5~;Ts746 zh|ZPP2pe(a;TLDsnk9QQzzI>9E}MC5MBemxG01(W2_VaZ<{K{$O{tS?^s(B$D$DJY zJ9^&(*mKI_%WV*k5(e#b_&J%tLc}h-9Wsq$b1OuaB1}ZK zclkZ4&`tJAe!?eq-=OQlvH-quRocaB9Q$EROD5qA@MTp|jMO0@P5>PpkqqcZl#DoT zi<1P;eD&|htw~$^l^$4yg)S^O8tT)Ln;VBdB~azvis>v^aG?C`eTy-_zxXjP%xk3A zt7oS-=fvrNg-6ovsUs6hX#Hy@3aP;Fvs1fhi1>DHxk>7VIbfaJi~T0if>mZ;W~8`! z%b;3!ssAi{x14CdN`AI9f_t>qz^r)%oq>7;!eMvF?7uxtgn1b91q@4^M2Dr=U#9RB zL$RBh-dMN7p z^6ntQF9!}I2j=TPlkbnrdnq2Aax>YT_!oYEo~s zjDh#&vJzsJb~frp3Q8L4YHC0YDG?z_$=8y`AOlB=ODC)7u#`^47P;*S4f)VRz=Ap_ zJs}cn^;oizZT3qs?q4b!{D`Jt9%#Q)sd2%|m)*HDR44ofcwAQDhqvWyv>>)&;!zfmbrOP% zA(Xix=5m7x0N_@9u3Y zqvT50U-s~;A4GYM!xu%{KVSA~sI)Kso;p~_Hy0g~lVoqugjhI(FJb3bdwfSf?(>dQ zU!y4Y_3WdFsE%2--?UfqYnN-^ei3ZhDTU3)K#5rHUWS{3uyA|smezUh@|R&WMMPb`{g*~16@n&ll2{8n2baF7bn1)kN;UP zy!iV(REd#7m5A&2#8|q}me?7@-(#=FE;8uI?mbMjIep!e)$>cvP2kfkyBj-eZ}V9K ztM^n=3s-$Ot-w7hRR2KfllIOV^RU?;ToFrIMxWoiO>krPPuug{TgrJlPFwePC{X#{ zH;LR^B=xjcubafRA9O2l9X{tBZVjy?FY3nXJ?Qe;?kOLRIrGquU0DiZ5=cnarko=L zgx1!HEOLlXgdWEd{4{v( zH|y1qYp)53YHw4B+?JZ3ls!A#PmH;}F>q12j#d2y?(u4cavp25Q6M>(6OkMp|4@*XS z6a9>33M8K?c(KuBYH@kN{j*Tz{3)BS+k8>n1DE_W`A81-`wAxO(HNc=$@SM?CO23h zjR=yQ6?rVY_=N5Kdp!}Ti89@`b$rsERgzbAdnA6x#sWI{-WLizEnYrndK-12cwW3% zn*>NPJ~3;qHU8jlEa}o@Pt+GQYd@StGI!jNpO1Nz-R32B*z$JzjWU?GW$oQY`R5>q zau}V>6Ax{8+KRV;zIbK>+EF_5JwzpKPlUF12-yD%BinhNaeg3v!;?8{?KW zduxqr^OT~cO(yk?WD~R=%R1me7}kprCnVT4w7x==sDJ?Y;`Eujvz^^Zlc0>YXA7P- zwn1Mw*^xO zaw~rHXm0Y(cNr4Ej=gocm?~&!^A;;5+QKPlWwhLr+Y!7pGj(f?eQ#xA==SyWJ=~`> zhFh*HFaU0UJ8pTME)_KSCd$otjf?7f01~a6O;f zJQMAwTuoGY9m|@bzQYGGW8`i>F9})@a@uNc+0lXFwm1g{tbkyyc(4xmi9_@0N`hG6ec>E!oulYQ8s9O}O|>@E<4BfA z&0n4)wkv$KD^2s<3O5LG9tOJrf?@cd!2%Qfawe?OcIKLoV?~9yH-*!Zp93#k-bh^V z1xqxY!id^0BsvJg+90z9?du_)LGHtu0YPIc=sF74Ze#WZ2Qvac-~qmDSaZe>gXK!L#6#XQ|3w=lZ=K=CvUsXCvkt_RyB+`3ZR4*bNRm za=i7hu&sPNn^)|#9~DEbOon8_RN8PL#Z%~}ll;FrAQZ7H`bHNaufZB_hZl=pTKS$3 zw*~>{_J%grT-92cZ>Xur9^aZvWvae@hxs<^#st**98cxA%RJw1FDp~`{o)Jd`w7KB zj$==z4wi7Qfe-B`_Oj;?Y8*h?6ETh2$f3ta)=2i0)Y~>|)!#CwS5d|39`;c{u z5a80)zx2-YD*r)n6s$kgJtPz|`8MlJjC(x|uPHp8+EOe|9t^$rPl9bbBe`Gif8O^t z)vCG5L`Bjxzc$SQk@@F*!4ho^-CFO&jrfu|GeDFq`CIiOqcQi((@Fvw#-QD&6E?BJ z?||x?@5+SJp(XPAkH0aj17w_caPtRPrn48vnE*#5*wLty1#Nv9EC<=hU0zz9@&Z)e zq;F@PYl*O9zg1h+ah9I&`KK7>h&AVx=$%cxkRF2S+_j_ddm5iTZ(6WA`7SQ*bJ3uN z2dr$Pkc+K9iCrO{0~qRCoSfzApo^3AN~68mn7CN&+miO3Zy|?@Fn`f@p2*oM7K~c{ zI4=isJDnRSP)K2}$m|q5KfV@f@~XLA-$*bvKA%1D54?gaX#~CZNOlO=i&r!jedA*w z;Nx*Wrgnc?eNCoqQutESzL#bZq`fO@{|!IbAY!u27fWp@CV?XjJSBQY3uN5Z69`_2mogQb?U0OJT2eshK38Y6v;^uIx zqfyT(s@oT&Qz48%n3GfUOSE09rsuL5Ygd=6PDz<+{C3_p`(jgfM`VN|&ORsG_1=a7 z<4g{qE7*iS7iLFv7(8kgB) zoD9F_oSo}I(XqrC@8ucSqU?=Yzhe?qVOY3lt?UqTxtO_8Hg_8DAS{amr zM4ShIT^j~51(|CzBl*sh4|q!`nS*;j^5enE2K(mcl(a-Sl!8v-UI`rizcQ)W3l4TK zE^izw-4s@Qbp4rwJmr%pDy!=PNs^J2#q4k<&m&Dq3*VrcmYbh$vRyceME|xI|2;7^ z^%0sY-sn@8vTSQ}A7nI~T3#w#E83ygG9zsybXbdhUAy6Q14}#_7t{>AGi0t@HhlCY zT;~aoeeu+6Eo7Lgn%3J?%8F6b*)ZJ2cU!xP?kNArI1@81tQ7Ja7;-4GNb?<%aIuvl zZX3_bbFj+;*%G?HENXj&2KQMgB7WL^eZ+lYeI|*dsn|e!lYNXqRlI2`a3Ywr73*yc zzkQWO+JYyxP9k+^q@FWH7^z=maBCu@w-|eeDcgcduh&3M6%-3%ad!BD`;s82oZR#1 z^*HzGj5n`{J@5V8lGlZ9FIc1cy9i^p8(-}7weCV+?_MuFovfx)KR#K}YiMn4m4w}? zx4x^Y2SZOY=u-#@*QPy~5rYyhC}8X0oH@EF*#>^HK&7mmS`39V=E$>X_ZcTt=F-^= zmy%#v%#*wFCR`7TY~H~T--b5z>nti2`LwqB1Vk5Aw)`y5x4=Fu7F_hX6S!)h28rjL zt*_^$nsJT$Pg%U+(f8QXBX;ZPW%kUgWR4{f<)=vGR!U!+7-kzOY@}l^vHadsI_ah6 z+Zt)_M%$q@t1p;m5p5!_R%zOKYRQ>Unyubs{Mk#yh}t*EW5z8y+W=Waac+8oOU~?z zZrBhg_3^rnh>hH`JiG!W+ci`&fXcX0UsW)jXZ_HpU1i44jbX#y+RS|66gEKKnRPQ! z|MTrGfL?LVG#jcO%%I3%BXVtF9br{j%~hph_3GE5wa{HsEr_MGUz*W6Nk59Ps!SQahE3);hky8c_GvsZwmgqO2|o$0vR)&iR(0p4RRfW^aP| zRJNq#t3TK%kcM}l{U+=ph~cG-BJ%XS&`qlxqmJh{>7BPKze)_O14%D0_ZCo~%=BFH zX2m{&cHQW6sPSA$&#`snBYVz+B`rRU9ie@DETc+bGny$-$oo3ebo;j?NYzA* zLmcGCl4vDw`P20$jPye9ZQW7-w*vq}NqJQaH%7?@{lX=Iwe{a{bkXU>&}XLi z6+MHLk(C0TR4R5DWmM<+sMP3TB$3^`#r}PJ4>L%k=)7g6K40WI;HlcU@L1YDExkde z%E7l6xR!Mt!ZV#(^;^1v=rqI=J;3)ck4u|Tm9E#BT{CNQo{^&z z;`buUH$!r5OVn6${cT0h=#9fZ=htGq&`!K))~*or-m5%U{sJc9Hq6ctP<@@_&}*wK z?nea?#aj3}7H%lL%W;%*GvptjPFV|%l`S;eL^UCg8QEGC-d3rp{Tzro-Nl*i(!V5^ zHarvMDpZ|refyhhE@zUgt5aPBU;JqgzsNEYzF7J5bpsyTdJJ}4!sw{1P{T#{n$v&| z*sf=|%G2y@ZlBwkCCOH4D7k8Dox55xV`h7h-)s*y_=VNcCS~zm;e){jc*Z8TNknr+}TM14`ZHSzuOFt zgCgBxA+XD@$uS}@30X7*G(4>O6kosPnQ5og@7IKi@21>xCPYf3t`0}7s=q)68E#HC z49?Rbp|$Qo4ObyClZbDIns^p{Cn)!s2ruEfX^*$MOxSa&+o{1~9IMbg7UfI}E2>3F zxWpN0F9A-KMTueN_0LY>ZZkiURM%mW>^Pl@%^%FsOOoVo-bTm7kNF3vW@fJm4(5=V zc;y9zyK3IfomBJsG8~OA6X?{CKx5~ z&Wz6>)BCdHYWMvWQk7t`qq1NpSEuAl2dBvoQeKoFC0WwGNf;jg_HeCK{7v`zSySTv zk4QdRG~})4%PrM|4$wYc6R7HzXV8`eeL-Q5j-QkQIK#Wt{qeqJ9Uoc(Ht~#UvTnJM zRliU0qj@P>tj!iv_DlDdqpeZisYkc%d%w2E3!=jhe4#_Ave^#9rNbxYP23aw)2QX& zwQ^gT1!pfA$oW2V{8Qd2m+zPTj^7r#2aJMHkf zzd8rr(>>Y6gaQG#vUBsxwGoHNh1OqV_Qn@K^_Q!pIs8J%FSRo%OJ=n8#Mp!Y%6U=9;R0Kkqj%T=FHFb5r$YMd9aYqnMe9rsHr(x@KZ3Z z;EBya3(7)DQpnoYU`Jl~Dq-ycg}tbYY(ZucXqwMw#?KV5ah1Um&@Mai?d@o@V+K*G z5dcD8BWzQ@GMi((@7+CRyGe1MJpxF+Rd?to+zsxx1XL43YW#1C*94`p zblB@N=4Q#t2X-6v2A(h5zx(wD)aZQ{FX)Y)IbCoaw{VeDD%uoq1;6$=ge;PXrv>#U)6<-lRUceNAAZHc5GywI z#}+iENpbDV6n`&nlP%JSQT~u+C&1gU!OYnus=QZZt?~gg`8PIWur4}m@rurr_MT*xyfnQE;r^=?lH-XvvT!C zYQxopz~+V$AK{=%91$hBLpzpHpYbGhe^R6Kt6uAsAEXoZ{1(+#ZoUn3Qnpe*hD8~V z(ccVz95Brb{6X>AZboqg&%S2KYvNa0ixm|7L1ZW5uDRNu;TJ)D$N3sty(Ikb4fKoc z%(+fX!;1Zj0Hs-B(;vQMm+BZ?ND+|g=p&`9>CP;@RabBrWB%PPu}a*uC;a*8fN!bB z7b70RxcE{Qi0>j{#iq7~0#5B3-s_{SfNSr{xa5uDy=&Ol-P((K2j6o5h7{)|eQn8r zGlWT9cq@`^S@tk{^n4)B_Yw@6HQ76=cHq4r{GlGM6Je>J!|kAKw(eauAEDpc)n3+r zd2$8Ts&{z`U8y9zYdp1H282>veq3sI?aAQQmTBEmBtE_qu6<$R0^(bvHRqqF@(5bZ zs@VDYTgK?qGzf7+rlXo(z${(#uQen5Bh{pHLVsQyGab5^GyCY;luQMoD-^7jzYrI~s{c z)*P&_*Cpzeqzsc*9umlA^T<6)i+7*i>xBm2j2S;QCK7WbKt&NkqMl?jm7O)iGw-pI zl45rK45&w@ug@5|h_-gq5}4WaDU3IB4HI4x$S?_LpE+ppyA)9$`B_0+!qjNN>;|Sr z!yt?bj#|BuqTgVC;f)4XfM^HV3zC&7KQW_RGo)GwV*Tn}o>o>|mKsmWrB6G7nJRyP z37`20RL5XXe04VUE&NetQsd<#BtQ(d#9?%})$vwh>fYAEvxP@0`)6 z*Rn&*Pw;#ndq@`!nb0FG(qKAOo)1O$eGs1Z>_0~9KG?c_EB0xyE|pXMvbpx#*UR5{ zAVCcrJ+CZxv0I)Of>RC3dF2CTp9GHHC4JW1`YOovmCeeb*#<30OfgcFQG=feA~4dr zbQz;@-aKN_6dq9BSvbnqX3#a9KY}>r9s1F+^NxjdnL%yro;Qiv+Ei(KGq2kzXZ)ms zuD1)Z1n+w#zpdS00Ap51(eMU>=sD(*_n8=+gmr3*Wy2Ks$T%qV-*%g`{1h?k+B6wi{Q$fPHMB$pqk z1e*E!`?yZJskzOkpR+7p^_p8bKu}xZqBkY8ZjM>~r~1fUs4kv>MFvq8qYiHw3qM7= z*|e;Nfn~KyRxIqM%}y_O5eM&y^3@ln`A4FA8@cmr__YLezH_4MoUoTY zl114Us5k_nzB|gpzb`r)j+L=RM?J$;sB1B@b;9N$^%6e8ARXvdAoJuR;rS#WXs;no z3?WM3=wZ)yuL495R|$H%uXI){O%r;pIa&;L_7}gc08BDq#*j+elDN4j=Cgppt`Z5q z-T`>iB%j!E)ly_BaNlX19g%q+Ob@G1b9ub5&5<1`x|-}w&#=6Iy;$=*9wVhi9kq&P zl$8VUj2haSrL#z5%BBR3C?u(SpRrbGS243bZ<{yGj**>|djg4uj4K3bP zM}}q5*>|2{2|n$a>1*@K(2A_4y)d#`J)xNp%k0`z6xx>8XFhUwxO%b_C)C0=RL*?dsg0;bAV0Y%D1IOyBv5MMdq!Mn)l> zF+}nGP{qG0O?m7F9ssjjoSu%b=#hkcZ|iscnSN<4HkaJuIAH9OY&zY}dmvm}57NZ2 zZ@ofWB!Ej?%zjh8k2HQA7J3|(dBE7y9xeGqp~C^3UkXc!U{-@}zrx**{IU9Z?-SCJ zTYC^aSYZzz_X=>!@Q&{T&AgKD_cHV6efypyZ7->uH$lnu|?*% zI`^W`&`D-<0(+eaw`r`D(H#dJ*B{H$D+6Bjn(?-`*AliH#UaWr5YOD?PUztlTe)by zWqj1QckyOXXOq{Vbcpf#wh(UnHoT>0)f@Gf&By9{mr=d46i>lEBz$H%+`d};x3Wx{ z=GEm|FI!bmt0=vg-spi?^6JIdYAC7tcJc!v;Ou$1-aEFnyE$^VzkPjzs72*=31%h7 zTj9HhozSe>+FX+cPSVZBX2*s_20gQ_nWCOO;YYUlFyOSS?s++H9Yb0kxf=h%de5?t zP+EI=tsvavXiDh~L7lVR@`7lkma!VV@>}VhLNhz*{kkZB0L21epFyv>O8O(zOXkNh zJ1_%T8(Yc_#698Ev#_ukl+BpsdpX?+!Jf|1Ozo|Qh`M)A(OI-_ds*Dg!DFEY6QvpE zCll(29&NHvoRN%gfsUi%DEl5Z zz$*h|sqD2O&*pBWvkLo`01LU8b^gYDXuSiMp|nd%D_BW8;PH!=+Llfulcl!K`O#d~ zL%V3p4-R2SBMil+9Gj`o4hr}-P4s1>w9#ygg$@#tOaR-oP;*E5ROQ6n5kHY;(vOL- zAi|5v)0yaBV~s>lBj1+`vZRh(x2`}MT?1-bU_-Uw@iG>q92((wV!UFnLE*}w!TnoG zKB~jAPNH#TDbq3}nztJ@U3W*D^XnI`a{*=BfyD_NU0Q8myY_o;X8g+ycPZn+{Xh8_iK-Vk`uoj%aIi<#;e)Sda#I5iKG!EY z*)7D}mZ|*;H52bzYSQUA=<1{jx)Eq=9D;2b0jJ8>pA?Z`I(#@sBfj~N6vnxJ6|uo& zPn#G6H-q1_{%Z0&S&DguZBfeo2JvqOri`^})8SIBIdSQq(>eFe1vV{Sgq>@P&iFn4 zK{8R`^D%y&n-oj0Aa8w9M%IdOj(*(gS0;JzV(}yfLWlx_1eFd-bH92D4^ewXDID#}$B|+`biz#Uo|q zL>L0*6mk`%GUj+|@HGGpjf<>6(!Y`q$eriwLV1uebA{Y>>7}y;)b5BF0p=Z7PDr-! z)Y}36xtnnRb2ahjZsIYbJE2nYoJUDhQB_G+SX5Y5RZ(A7KtWCJ-dRM9hhIR{#7sq8 zRYg@$RTe13ulzP+6eHZu#J_7w04!!C`a0&QuAg)L z*8=xXk0wa%qXO$|Yq0QkW^Xj4qSo%tTAqL^e{WO8n))M)-gp9f5gTR=eme!G8Bsvm z?gojKWjpPz(AF?<=@;A-NTVQ0>{{ow@hwdhoc5B+?ADsOOeCa*)l1u0+&iFdYlO~3 zG*@VMeVdo+z9#wE?R5Zow71F}4@o`Y;}jaam*pP3{2ua|-}G2iMh?`>%}t&xsGBXc zcFysz2=W?>-?(xoCUTt(594FprLNUkcx0K}J@+90yq#Gpns6^iwk~u?Rpco$!hn8) z<3JVRB|q`1odathyHd=7k6kE_y>mVzoM)eo64XBW*axCq;56GZ4TtTNFCf04H(}3* z0~d{o)IQ!#`-nyKTQ~%-vMvRB;QXLSWK?&p)v!s?8)-MyaoLX^vNu=3hw7{2{Hj^pLmp2km=nYEi45|r<&Bo(Vz6;{-B5(c+D7U zt2D#|pfov)P)XO%_916UPOpkn_XH1{CbH&FTrS35xu@$p=WLaiZVoK!o%cMVNT>~> ze?7pOI|t^FLS1Kg2M8$0XtY>+Lr~z;rdCa*^vxRMsRg?Y&Wbok4EFBH5_yzIgw7bn z(vgmhi|ojWop<=!=#g?Y>JJ({t2al|@=m?cihb|i!ohWS^Ar}t+}qGT%6kSe*jczE zhf_~=fvykdSp!%-_Vjv*>FMeqAFiY-9l)PJ)KWh8qd@iS22gw5bf&9MK=818fHqNb z>BvRc@53(*Q7)3kT^0Yz6D;m$IJKUT4{6cv!+76s06!Q=wgzWph8t2qDhuhO-wPmm?90xL@c+$YjpA0+IJ3O+9PDq-ymSdkA5pf!w zXv}h6H1k`#5%8ihhRcrGoUsJh`rPlW)t1^ixptf8dVXcHs>!`iR_UN2vE%`n;zW4V z)@|Z&MlVIBj`e5PjkXz8gN+)&3nufK(=)0zP4^z3IhJNLmrWWf#DX^x(71t`P+pj4 zY|o=A8cdpM+3pih`tZgZAPPwLT+gCW^He&47enByhHK%@uKz&ejBuA1v&9V#blEU^ zf9K2$s=>ih%Xwj}taHho4+nK@&4_p99o-64pWiytHwHmof7ic3N?~BByag5TIt_%9 zcFM}TW^dM0EI7T5qG>?#B@PfIu~92Q1^4$Ds$PEyk$v`QXp?MwTmBwY)NXMvq;NtB z>FU6ZdYnUohly*G;Ze1%;HlpM{HOs&4jIiDH?#VEwqfI)*ODYPlq%D1sv;e3*@3rA z?|T32^YZ48P~@*n9p+UHgUK%Pb0qb|#J5QGYA;9imhkptyyE63va5=B6E2sx#vBiX z@@^_(iWBvP(PN> zV0Ll=lRjU6vVe2>#B)E~M0{bv!a;eTEqyvbsMf=-&ilIH)|d;<9RWLL`uQ0gR9q`E zU*~yWD~mM3#2L2Vudx>YJa~YPck+{GwNt z4t}=xy&BFEm!@6=EKEH-&1LYa;{0& z0t1XJ;?oL`F+4qkGQg@E%@`kK8sGXGi|;>p_~=?S;nVK^sH&Q*QYNFHFh}kSLj<>a z+?fR=QB~LjRdQyEOhiTrOm$(?bZ2KpO1!8dSH)U;z7$$fWkp1*QO{b2$T&`xET*H> zA{WbSwH43R?gTA#Feq7awcc=F*A4yznBzfMjE+ z>MvSuS%Q4)WeStR0Jj8bv6=K~5;#rZVwZRT0MZwgZa*}bfj)59-n8iHQB5(wc@MTW z=F=#E1K#S_5LA@D$N=8bTq>fcT zFiuu_=+*a<+||kQ$^9wosEc>?;wFnl=~92#fPZDuufKBqg}Oe9p#hW~WvBsJ{F%`N zf)X_lV4Ug*jFpL)dB5BX*Qf1yPTsuc>WtKBzq0myO>263zAhj+SxP$RXTN-(?U+Ih zyd7Bup++jJx1w)RWrJ9b+JR*i@wz&EF;{=0qR!QiEckORZI+qg--+IdWKk(y=%KU# zlpz_Q8Dkv289j;|NzQItO^$@kP|~VhE%kCV3`Pgym{?AWT>EAN!^j&AQ1vORM>| zJNfsHt;d+`s3}!_s!10Klr4$yFmK_nx!Y2j+%BFqs$2DToxK*(>}xw3e@=QF`@VX* z`{P?@uIJ6^yPH#AZzqR$mE-Huw7ve`+Gp(S{i|#1t)Ei;>E6m6-+RVy7KBzY)#TO7 z7~iUj=#L+CF`H=cuUAj-E16Q`d;O$zw|~iFdkY6FnJ@jZkH)prSZ|Je{kFM(j&t|I zxzlYKaxSy&1Rc*tvSy-^HKHBomr^@ zd@-xmP*3wkwx;wf>Y|3f{YP`i9AcCkxI}BL(0*9?mGJLp{QWlG&MaBy95k~L5)uaI z$>$P1n9CFW_VfE80_k4mMf4u3{wz+W34|d>t&q(npE2XeBFB}u?frLCwsqKi#-RP1 zdRL|BgQ{lTOUh?rm7N;+?~QwPXqVsp+MO``>*Aj?5&?2IlqnOZohgN3$wuJR;N^JE zAN!&Gd6Lqaqi$&S4zsrTv>d+=b~kb*6w~%ptSv9D?0;j{`^%HJzn>pWw>+e`#oWLD3qm$w*hLVrb-=W|3Z^6?+VZ~Er{ zP*am%Wn}z)1(YQ_gR*i)0@Y=#xQFxx#_cpiF=Y_(++;dE7^ThpTTYSH>(z!g%BsEV zm8+IheM+*LBIZXBPykFV7o6(dbDov#uuAN2;ouLzF)i8BhYj?{{n**iMkz4o!e(0x ztq|v)b^Px5MQoPh#G$R7+56+>mnw8zUcOy^f4w}8RQ?<8jEIQ{bSb+Qy_3+7AuAzd zV?v$>pfV~R_XitHUr@H&K%G`^@bYRAT5F3il1G^E&|1-q+mELjA|ZFhL|cz^z- zF#2mb4s0naZd`pm28JPJ$(ZX(On(O$t$goG zVSA?;uV-!TQ{IEM$h&?Y&p(g*{4U$ex)|5@{p=g~DpfUn8OH$*(#bk5sD?uFw}0vP zSrRofGVGP^lTt~`OI16D&MJAKh`O`R`L1NXYyhqteA0$bBjq>E0c0hY8B_|nD&%g2 zHA@xW2aqjA+yicZlL+T4$yiIeK-~;+I(n?eO`%{8;pHP>;SfTrJbqF5W8% zR+ci=0Wk9?gFkGRHsXTgv)AC9Jh)+|7mM6CmX*#EdUtw$`kSD#(ETgxqg-}GeOs7ds%e(WYRbQ*?uH|T*7wchNFR%LOXzcL# zh}p+>fKTw#Bj|yIyOIu4$Q)-cz}NIOx|c@8rvr)2aTL-w3f-c#h-}9zn4XQ+a!KpEe+Y6Bv#gUQeC(291WyjvAw78J1;KRe5QqCfQ}zm|EIG z*yrMa6K}Ck|16m7L)(>q?YRBKk7niJ$pqDDjGU&CkM9;c-;w7I_ubl*ColeM#(F>T zL)q`YIK}jHI@~L=R_F9)A9^9_%IeH4JmN`gMN69$V2FI`>ShJ2+Ro|~s0Of#Dd99g zj$)KWYc6smoE2ktf84_*0(pt>-Du2`CS4jY5deUPUSr{KSoef^VoBp6S5q_P_zBi} zda9>@aJu1mdg@of)-1j3?Btuvs$8B$KJeGM3b@{XhTnG8-k@YYqTo_|z^EZm9u0t? z2H>4E>@rMpK0o7ETY`fJ_n$q-M>a^@lpsH%OROf??j$?O0geET+;)!!p#h-afaBmo z<^Y2LNC0#hQw_kxqKdac%ZgOtSUpt4$b}>1f|8W{_Ocps$~kb`j)I%?^G=uj~s4Vw%5e<%Y6O%wsm=zW4p)b4)HS6i)GvT+VtYh z-1xvCh|yUp%jGdfLeQF!FlUCX_39Q=FsYOlK5Fy+-eKsdYHj zndaF~GL`zD09-XC!S=tdcE%-U3IdSt_A&8*C%>nM)h1V+a_pRw&U)r(*HtmM{Svk% zs(Inr-K=Kmx|Toy953<5zk^@?9QZuy_rAZyrS^SnXfnRn#sE)eXHx(GK%D>p00000 zCt3gi1^@s6BFQ$oD?2wiJ~}&EY)d{pG(A5$YFz(l|1bYRJvus9Z&pr7JUU5C|5X1k z|4jcPOf9<@=UQ%rL&>*9d9T$)JYJuUVwnsg{<)(^t5>aG1U~^H^3d)I;%G(sROkTa zi-A*gFF>F6o<{*(EmMs=*FlU*?mDQTut`s zB>Ulru!&eq92CA!r2BSDVH4PLSIeDeJsQdr`C0P-(#H%#$qz4lh@!}`DX-g z+2QTWR>r4k$iWW7m?e@7LUw4F7i+DM@(nCb0SqN~xPghuH6a(o#vv2C{-Yhoo^vb{ zi43aVUbo+!Eq-7zq_R;IC9jVAu9)Rx)BHTUhKTmTGwK5jC2yUO9r`Nd4rF_#i2ng9 zD}4gkV~@Z4Hlm}g8|r*iR+NE~5u=bbHI&x0)Q0-HAz_C4y3NOMT+TE#8>Q824-rSq zC9zlc{lf16qtQO#J3#w$ExoMjg8I$4=I%4?f6;6atzX}2HB079Cu?@|zCLR9YH_pa zkR^Y}(PpZz;xJx0b^QH!jv5|MMib_UlwYU=6g79bB>(0^k)#|;w(u2cfzCOv`zpGK zHcn%Wxf!3Fjz@*j#xTr(C;P9@-1aGMBuU>M8t2Bb{o6KrA@ASE= zY0PwE-x-@xlQ;c0|8At~J}j%M7_VjgwdjLmQ{_2V95MHcV6RgI4NBfZdwbxv?%fzi z8ELcCc&dx-*bcF&`%=R?wNPzxeNI{I>iBAi+UcTUd*amfwl>m7%YoOz-;tqqpXnk>#1BeYT%)9hS~Hpt4Xc zqZqHHqpe&w)wy*#oQ?FYyj9n6&H*GXcO$Bg_K2tC5=uE+4&RL1vW+T9^TPGIgnAxM zp&^h5=CWHr_2No8SBzAv_bOi@Qvd`pZzG-W7&Ib>j!%>pJ4hYasR{WkV^CwXHC->U zwAlGntee$t&{?b^CAy5Zq}j+if;_?4*QivrgGf5dKzioCW-}yV*0U&3ON+l5C;q;Zxp%iV3=7E zav$5~g6w#*PQ@aTL7OM*LmRfK9G9q5G_|JXXN2gAovY(B#*MVg9KmXpj+{0C3^n&B zdS1`V8Km4-_UA4bDKQSe6OR4!`wscdePi#X=2b^m%>~~4|M~>y5o=kx+s4yIM@E_} z5573^Tw%CQ`qUlv8WTsdlwPL(T*LF5-?_u{`oYPg4?leJa2ST+O&A(fQT-6(9RUn2 z7g-wK5YLWUV<@?6ZB0@0-vQQWlMB8F95i3gzOi1iif!XY{@?91YUA-X?xqdG_(nsp znt$!r&mz@GH!u66=a(EXe4G3BnZM2_@!&}u_fx!Yzp2il!wy==T)w@mYfiR@O{9(@ zyewpnf0^`aA=3IXh*^E9BeD4K;(WBM{Q9Bw?uQ3gzy0}FZ@%#MmB>zhE?=%O@z$Gt zbdbILR?-2QCszsfR8`GpF^r6is zjgAN>1VY!2hmy`JX}i+3x#V5YD$J6#_m`fma<~>S<1=JdvYSzik3{DUB0glwoM$N+ zYxY|inZ?pdW-B8dBLZB+BB9z@(`&POr>+z=cu|P#G*_&tjaeBAV;5O1#~DLb6gEI5 zDk>`qI}@$#!QIs$CQGD;4AnBSs!z~0vaEIIrk7%M*8XB8w0J1Ic*m)*Unrp(^v<$3^XG32DIjSJ*72V>-iczH<26A06qx4d5MyAb9$^H2<4*^u?%-K zj4jR1(iKfh`%UVSDH_t9&v+HkRr zHR3%6kxnSbuO^m!`K%<}^Stw(?)z(ON*#)iWrG8*Pd@pCjg`0auCzNYOs#IBW?h&( z#d#iqV3RIrctS6SppHNTAme}r00A&o2;KhQPWm+*t41VRs*ngh|D|#-MQM*k5+ePQ z7CszxWq-B%r;l0}E}GWk_2$WJzF(VcK0p5S^muyvwCGKT-9P?Y07y1&rPXS4ISo?%v8immenh2<+2E&L0zQRT4Yjo zjtOL<^pKt)ppzjJue??+-BhNlgLHL?tg3}{R+mhepz0ae>DJF!LkU?N59{@Ws$tGf(pD$~#yaD6NezWDE?uUOGCB0cOi)x!CSfB!S!(~`PW}{@Hu`cOVcKa>Jm*I&xi8}b)Jq;8&yFK!< zqW>&!oV4c3==JQZ96?o{L4fJ-{I8dv>rcIYzVrE3Gv^ul*}~cd3MC8#IlKL$+LdzsJomD&-hgJz zAAlPJI2{=8zV|0UWPgX;{kg^CyTu1{(dp*Q^;Vi)+Iv&&F7C`9AeYN(cUw!o(=8R} zTX>25^?z&bx^bz`{QUo0{rK{yK6z}H9;&tw87|_ldi?*S>C3?XHwU{91UaiLTz9|)(}$j2&DJ-B0Pj0)H|sHPZKKs5 zp($;mnjYUVHfLvjRL|qGZrsM<;@<07LO$`Tf77IVje5Q|;7Fy?wqI?*kM; zt2#<&Fu@FZj+}OV%K_sKusYAhTY%?ud3N(&3n+P~H~JV?{$y}Sck&n4dyJQc!|-&x z3_q1Krox|d?f-lLSN79x-~jwI$31!+z0rD>dl|8tm>)pRVj%qvpfR;)xRTyoMBB9! zC__X~c5Hy`pZ!?WoxKw@26!KE;7*h*XBzx8M*+Q0nHXa32l8E!w80M`%Ys3l1K4PQ zL7Em#lY|!$(Xcv8Cre*-=TppD&`7x=gALgE2^v}f3^`Md`yIM9hnx#1ozDsW&hTE( z#i**=u6Bzx?H=~|*|BfxHhLKyX=b)M+A@E3j>9;PS%E;x9r*vNw!bR;aRU}s#l>%q z0}MS=5@Bd%6gj)>@{V};`S=02<;vOg%>c$e($9rnNc6GRqBc`Dy7o5{^8)>WZ@H{9 z8f;qkk~{VPW$7!(P;bh*(N4(D((9z70$Rwt|8=4S6b8Z~5^EX`nfG5*v_L>SbPtg@ ze;L1VYPH{;Y#pOrxZmfoj$Gp$IDw1fc>T>K0LA8Fiw?uEY+@Kyh0x^f``(v$wfuMT z;_RQUj{mN=PusS1a#A&R_%AsGJf2*YuoUs0mFcfTK=N7FAKB{D4teIz@|_e!Z{?`o zZIatXc2X2nGwpq_CI-vr1o>D!6{9nC!p}uSEhP6CP;r0fywm z=;rBBW1W)-R!=}#fSukKaagC}6U!fSPLL!J&iDW_;c!7yy%jybNd+|xD(DAi5F-Ht z9>{$C(%-ExC;uicBC$`yAoTXdBwyi?C^}yM z&Yj0vKi+$OVs>>ea-vvTGDQh~Ef0;Scb{FHUVZ<0v8Wt(x?MuCD}VCFkV_2MWTD;l=H&V(BXMh7f<{7Pt=cm%u)Wuw5uA zhx$9<`fNB95}zU)?c&iT&^Zp!STLvs0B{6A+!;UtG7l1f)@CP)3;_PeeEie<1`tN~ zX%O+n)`G@H=IdWA?K+AFM0TfIaNb|$!_}D^KlIF7G!@>5yd9k;*y-sN3RZ~H%*l%R&0FI2G_1xU>&Co3EFTC3_Mk{jR2 zTbX9Z16=*ED;7);CJdT}0xK{%iCI8z5>ti()Zn3^Q}SrMaj8{x+txMT~}D_ZC)tUwP?v+(q6PN`#}v@?1Qs9(XqZrxd$%ET0ThPN1NYk;yN~!n=yv zbkh07FdR5)E{lSqwr;^bWKoqI&}C-OXZ3N6Z}#LK9{z@d0Hu9gp~c9$_R@(CLhGeU z`C6MutUBre03OJE{uL5#EA9RzXfdthPm@9B^*0g`kXZDh5~<^+@v@LE{c|9l2v8}H zszjK;&|y?82J+0Pp8iU6Z*BY33)^A;+IjFZHsgz_+gX0$xbnq4=C4pyTKAxW5`MOb8Jb)jf2KZ@2a zdYNrkFiYZPmYu0evH*H|?E%c4U;s&Cs!gcCTh*`fQYF2>a@3T-oPUkI4#%D@^<@xL zT3`W&R<91&xS#;IMgvc0XHx(KfV%(y00000 zCt3gi2LJ#7swo?W4*x9wF#jU|Apa2m7y2$<$o%@hleFKJ^|F;n912YeneSg(%C&NI z0+D_oH11yk0J@LR9TYn%&z5CEl2H`{=mS8>Q|>v-j^MTP1@@0+-bvY9oVqZV5l?#g z@K<+}2fyUre6v39S%TL0V~dtw-CWi&MM|8g%Dt|LaC&2l%16atqkOiKz-3I zOq$BdI7+IBG8QX~x3FQ{Y_yeVlB3;!G;?{+_kzjd>RG&z{dOi_L+Jwn*rhhAx=JkI zZvX%&kUtJSVEQ?*|3Jxd9;PQXo?`$&Bmnj<7@VPRIOO3Su436lf)_x@VB}NP23A_5 z=Hp`6Wmq!70*r$IGyopQy#J|EuasF{D2^g+QpddfF`@wCghj+jIPI^?-89!PuK_p$ zD%N6WRh4X#VHksozxi*t6z~9G(gVOZY7v0S?L7Ufxp*+1x2)CEv>hV0uWy}ML~?Rm zF<(9s4(PBk2ZeQTz@-A;TK%Kd4j!~B38p*CMIgquL;|TKTtV58UDb&E#&Dp%9&EKwgT2CDFcm_L}$d46i+I_MP z+QWeX03OGD{wY$g1k=9+%}J;+DP(^B=R^(QrmYWIjQBXzn9MT+n%$L(Ws8+zScYL# z0oWLj&GUVi0zd(t_n+fu%k}wMZyd$1ySJt$9psYqTnmR+A|O5BR2!cfWih)Cv=8_90Aj9Ayz>--WA$Xno1Twd-1X^IYUkp zU^A@cZG&8>#vlZfBC4*DWqOms<6CAxmpyak0%b_D(iPSUnTeptHJU8FqVv}$B=1`W z3kwNN+VNB*wyd<2XdeJj>WLs_-4;a=(ZARkZD-Gvs~oz;Q5FJDW>=8DPkcHZM)Dba zMyRvHOg;z$^SUq=%g-VvxX=XMa)7+XIkXN0G9iLE00n->{Q6sDj2pynF2fN}E8a05 zf2Ak^a>wE_T!>7Qe4gVS(CnJnSyc?fCPs!)5dij~_9rR!N<_&}aseNz;+^f&pWnaA zRIJCp{o8-JbIqPx&Vh%z^y7ds(+*_s_s5dZNQ!I+fu6~fI1q}|Sg2nLjR0Up5=iM@ zNi;V1LX&%q4IoVzBT*x41=>2T#3~X>O}f)Eft0g`M%ePTIvlcSDL zpdlAPI0zXK06xe3{#B%p8>D^cY`s)bGu|;j{uo);M#XAT9Ge=EeC|6BG>;H~ld4!I zVwl7*3?o7S_;)_j|39fBAVUG|-z=8y zIRLXY&KelU|E$GYaJQG5{Go*JjJxFoLtPY35bd=I3l_Ms@Pg$EWCrN06}PMmg_V`* z#D$U3ig18~vZerJGDenY3;-}hcqwu!&BR5wz^W_9@MIQ)fr3sL1BmFOymLFskehez zDV~vmBN1i@_QaH$9*%N>;GE$DeQKKYt&X0OP2VdTsOKSVf~-&}#JXt35){G=P_VrD zkOa~UixWllBPqo#oB;VybZbBfKF9p^GYQ~`m+*QZ0iY8Usa7{}$9?cmf zGP;43X{ex#Aq}T)1h}dG)-;(zf}>GEgAUf{3LYy@b7#M1GC?5Ie}S9If|64en9nUL zEd!)eBrJcFFOzyemQj}fly};Mxr{ggD8kqVzzQA|bF#t?1OPtA{6Al#04{j28oo{w zjdRS+zb65FG!G?fjzg7Nf!Uhf#a4}q*d!yvU;qFhDGdO?=&6Utdy%nb2>?L-z7Bv{ z@{rZk)>}(79~|ex0I+CSwN56w9KcmosE;hlXpm_v*3LL8#mm}EEmJ|beP!HZ7;q1y z5poc!WMt9nGCc)ZK`1~tSli1$$zT$CZVg~f2`^J(O%{1(hFveYr>(UUk~kP>ch&{8 zdl!^IZg5oDZ$q^~mhs-6P^K2L`jY?vDDqA~ruWH$eG32S`(?!Srv6Mq0-X#8XTKlfirfJr=n ZA7}(5AR7Px0000000000XaWF0vk5GN@6G@K literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/favourites_timeline_updated.ogg b/src/sounds/FreakyBlue/favourites_timeline_updated.ogg new file mode 100644 index 0000000000000000000000000000000000000000..a2cda8ae5252cda1eb5ec52ba3b22fb6147aef25 GIT binary patch literal 27872 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 WobnTw6x49b>dL`ttn{0~76@dwlysD-lLIjn}| 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 U(y*|PV42GeD+Y*gU=Ps$2Mew~+yDRo literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/max_length.ogg b/src/sounds/FreakyBlue/max_length.ogg new file mode 100644 index 0000000000000000000000000000000000000000..895d48f79cf121fd73c8d4caa319ed7fb29a5cb4 GIT binary patch literal 7217 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 Lxu=qlN2vb`nnZh* literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/new_event.ogg b/src/sounds/FreakyBlue/new_event.ogg new file mode 100644 index 0000000000000000000000000000000000000000..42fb3c18936831fcc6a2a573c2e296abc1818bf6 GIT binary patch literal 10206 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 A?f?J) literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/ready.ogg b/src/sounds/FreakyBlue/ready.ogg new file mode 100644 index 0000000000000000000000000000000000000000..0b8a992ca9cd201e114d09497f9faebe04b9f59d GIT binary patch literal 30574 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

W$nv|E=Z2KF3dckPsP!9lo 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 literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/retweet_send.ogg b/src/sounds/FreakyBlue/retweet_send.ogg new file mode 100644 index 0000000000000000000000000000000000000000..8cd79172d9d722c2a591f515236a6127e28004ea GIT binary patch literal 19362 zcmeEubyQc+x9FjfMvyK6DG`YeDUGCbql9#ar1S?wT2hei?vRpHq~in9QX=u8yX*4~ z`u*MYyLY{{-d*e6zutPoFwE?k*>h&k-m~}FGY1t*OAP=S_$P3`{F|vK7%@PiL2`F= zHnDcO10h43|7miJ^e5AZq;l8uKf+zlyPBH|_{M!P#D59GD1T8s1zSjJJfl-Ib}%(} zR&q3B(a@0QVWnd+F?KOGqjPkiGj`N;)24gH&B?{Z3E_qqu-=V;g!Z>PtE7}R5`YN+ zP9!Xm{m=2w)aj6bEC#_i z2G%%w*0^PXIMkrvGGxH)jwD$u;mZP1D{@hftm7;^6D`A1ijq^-i!-qPGyzu@_3mka zUvIQt%uBo^>V~;bttP7;a zMOfDLd5^1N-$%{9I(_VaEdsdgU=Sx_m(~#Oi5G?u@xsO+Lqtky5T&vIM${k<_`@&Y zL>{JFB-%hRd18$JkOc~&CCkQ?!udNA*dR1}g)U_z`q-Jk4&Hd^yc!n6*E-D=T><#bsC?Cnf)H&hvgMMtSiM5JnD9!|NtD2HRQxNWmzc z(FObJjijssG4kLE*^j6(4{RdsUaeAmD~ zGp7T9AcBAA;)lUxCZP`t)10x`it^&Z+4@;(2e}8>?~=04VWF+6P68<$uFpRd*m)1b4>CAw{$Gg#7nRN|MgpDuk9-6tIfGJ>6^Qlk3%Ab*BcvYv zw+K6+0-5^O7pFcLkp`RVCH|-Qck19wfID&eBaR*oO`wDo2n}B59CaG#(g?K5!c&G1uTT z7~?e`(=wlMH=n6Df7RftSMR%c_HS{@?cXH_6dQWMsAakTEICoX1)><_;~4egnEpsk zadO5+an65{oGh#G!ldxR&~SKY6iZm5by!MCQ;uDK#nzYqWBd=v0R;w((&~)T|4DK> zxTvH+;i+L)I{G_Dkx_7WaFr(g_XPleju@O5cUuXgn);Iob>0c}Ct8{!TK}goaVJ_J zVG{s=8~{2$ul#U^eaMnyldYSKQ(I;1qM``T3e*;0)m@^83VyjL05Xm@*l<_KxYkBq@b&z%H35INc>BNz+NMY zL^+shLDtB^V?ivE#KtNAd%{8i0E&bGKmnqD@&9@UWi$C_KGz>}FELr02)IdIQ{d$1#L-qztc zCCH$90m+VbM|M6d1$I1Dt6l|wjuGfs5=dP2%vtx$u}UfFf5Sd-^jwWtNkdC3Vc9AX z9+tzBQczOD1`Y)KqoTC546IWA0e|5ThW*ppfHVW{nZpuZP+U@htwt;j8b06#K7|G% z1wkd7U&QZVAjp6;^e@VaE#!Y1B^W@hOG-*WeS@qCHkfiVC(pX;Qt@~Im~u0iN-)<$ z{{`0e*RX>N(SZcebzlnh71dx;DmCr{#1u93Kq$xyk;V#K`8cNcC9$%N6(!{!nyIU^ zvF2k*vOP*rmypZPQ|DHAlAoxK_Ant%p_ndDU7ZyTfKm&9zq*+3gUe8XL(tkYy;It7 zMo9&(wKSLe5A(@D%chvv^v=fl7{>(~I?4(gNg#|l-)s36?kUA!wO1l;`07Lic9{3 zo_7_0=;Unh4>cbRpnsMOS&dVgo&mXvILtG^@bl$-G^NJNlI9U#d-687Gy zZ+2_|P;mkPwkl({p!l5X4p7k{-|5RBE=*R?=IW)(BaoD1eIN6m*&hk22?7Xg%Rz(6 z7S|L^z}$U!*JM3RlQZqlTz zk1Rpm=1D!7?M_$GM}-$$-DI|$-D`J^CtpTz2{qh*5y{p4GKjFdi^R07Zi+sVu%_$B z%knyO)5z+kx@nuCCNhS|iYIm2wnJz@lCS_E{%BgDQ{ji+l9dyVLQgv<6irS@ZKExz_$v+}WqrtmcD)cNL(e`tgVQR+=;paK(cAa#X*D%2HTfcWml_*45Q)%>T(nm>Nm?QiW%Tadu&Y=7H* z9)D>IqW{~@x*Qxxfi>YTdT`qfKmo*|gb5@jh2%Ok0Ss8=Oj&<;&kO)?azCoeKamck z&nL@ElzjBSMO8hV#RX5jtXOiGZjCV^oGa+#J56qk1oc4)tcT``tmx$lWJ29Zxk)x` z*`_$)9xDL=0WtSbv9dbE(E%ztMmZ$lJz6jxA-#AcGRpGrALsxbW-K{9VKo^elO~FE zmn%@!*b##dkW)}vlah{@L$SX!Zi!<`U`i51ps>&(VKC3_f2YiFdPn3Mu0Z|weGUZx ztvp6b@#oJWE>zSsv~&;Y85kciG5@1>&;eS|Jj5a(BpjlW4e7y<3+=^}59_<9004C0 zb`1thIsggjkAd~4jP#FRqJ!;EEpX?TxYJOHl~CPF#?Q0V3_QE+1tRY2k`mYN7b;bDNh!4e zmiwkEbRpSc+6G#m(PHK3o*g3*yN`Ol``X!c`~9ha11BmJ`^E#>9dW;=L)Fy5y6Cfw zd5Jc+V)dX5xjJffPAq3tV2A=roM{;imJRe>l`Rb2OK*O-x#ga-zYpeFB*n$MXoU)o z{p8l|X5n|a@U2HX&umUIMW6pjG`zhtWqr^9E7UHrh$78hZUSjA^p3SLPCwp9?#cts}h%@j`<-K|_8CUl?$;AWu-a1L} zL4pLj2~Wh%qKco>jm@PjTPS*>d3YBxQ)z%eEblIzRKRK$uDgV+gsk?eLIh)ZKRLYQ zAU8SaOETMocw0#LY`FASvy-2bCAzx`T=J>tUAUZ3nbHw}9GQ=5@+S@uj>M8fkxT~c zLjex>7(F|&`v>*`-i5_?Y#P8>8)IkKRz_xK@{_^nvD(DJd4)^|R0vUre5 zpxY$BTx*L+bkgp_>5W+hKO20)p6Dd)NWJmYxU6kP5GAuC+~&PC`L;-;0+o0SUU*1f zA@<7lKBE9oEB4^LsC}!^#^z~X5v1158zme+OBB40SQEDw-KMq%UL@C%HF-l6A+1K-?MWL2-29iDWrc%~U|Kz5 zZxl|ru%@}8UEEv(4=$q_t|+ON(!vN@2uIa}Ye$`*_%SkM@xr&t`<^S+Mw31^Wsm1j z869GrQIKEC!~L~vHg^YXM8xmQ$p(& zA{Copcb%$uetqrdXJR5F0E4*f`=GH_l{DYVr$yrsBX6qaCqbNQYjJpJ%)1!fnm256Z9fkSx_| zfY80gqPH(bSQ9?wj!|+p^@<~pWzyb6b1x2!r(Tr#Hw-OmHj1g~fw%nfO8W%lyCHKa_- z6js|Q-tI&g4W};eIeT>_g<;lx+Zc}*d*VynpW*Wiol5k|WZ!>elP#@VV#^=to5Zl7 znquo!(OS%mO#Pul@ z$T7;4g?>xYds5xN4^Vk|rjnVPzi*)N@|4%q4Lu-Q@DeAAn=#c-TlqB&`C?sN5#CL7 zFjf##-=d^OPMXMT=%w@VC^gAN@4dZI`fSu#jWeZi7Ihz&r= z8wL!9Te|0F2IR=eWo33A#NMYnjE9UrHp;{N_0?g(yBbD4y%^<}a8NxfmFr2Y)hSEK zdYn4*86)kdxXOCNK|%V>Y2gN5UG#bMgyfKL$qo^D#(pa$b|in>Nn>=Z!yrofV=D|G z_?q3wkLN1S`Iao{y7=trZR(gB!JH!@-&!q#c<&{rX=?D&3TAa3`P-Os$S4c14LuQa8dEsL-KKU&D3_ zn5j}`CQ9*1g0ebzDjGk2A8g-a#>UwDmdLxv`_3E*FXx*$A%Gz+lQiMX)JYN7P5F}* zMZSU1p^)1Mpq>!m1Zc?TFH_4<$Nd;0$!F5b`^w2F(jPxFC(~MUyODwMWWd* zu*8tr_S-z!Ka=&wGie+8pl7Ph71kGKOeVvH?$mB-x8H-H{zSgmTwuq#aYeNpjX`sy zwyPwTOsnmlXxXz(iT5BHpEF)GwjDlTWRQvmjLYjZhE%X4wm8c!_jzGjfMJ5$@b6fo zsb!V5*a0ss2XwM&h$8Zvy|JhK^rI=oCQq#Pb<*QIKlkSE)SP>WMZI&W+wy&q4qwHY zdgH_QVia+{pKxqA(c=l{fEsveO5Kx5KE5rPLv-#A25HEz+47<(buvb@r+ihtTG?iA zR52c@;ZTu1xd+EQiXh(Q>8dnW4?x;!c-HPJhG$x|m?>tgD>KL=p7_uc!TIe=Pi8#- zucMFm-Lzs136Va>km<0Vlh3gLS-}2hf`^QsTnh)(7Kv7H z|I?SWWY$B^qu19aWhk$q)P9yu$K}fQ(wCb@-Ik^*wVNrQ${Nca4@S+36Q*wOe+!~U zq<>aMm@b@Iu1O0Dx-qVwbyXMoUE2;z+3V1l;gceVMLbB8U7GF+Rv zzVi_1+5vu6l0F&ZiikT5L*hd5BGSmlS-+ug5ew-MHHFyQQ)Jx}h zI*K-ty?-$Ig_!4ZXZDfP4(FJV<6;hW&BLka9}#+p^akdV55f$uuAGJSsdvRP^uZH5 z!=BkrDVRV;`qJj-8q4kK4rLGiSw-m(#{BP?AWyP}`vaf_wvqN|r0@7qp9Y#Oy-H-Jj&sh}#k(JK`b;3=39I_eu-r{&q~0*? zb(`lEH7hi75V=ReKi6gtf9X}(3pxd$;myY|7#zLb`%TrMhz#K305w$j%k488 zgQW1*9+72oD7v`-(!%=OjaK*j8c(jgUJl~y6caPo37uq0b5rw_So(e|7jC(^veC7+_w~kr1UrJZ zCsVK0@#Oj&e&JzNdne7|pijL=ZDWt#p+|cYBL%eYBs$)>O#u6Yl*;$LxU2nrrkn06 z?rZ=Hx|Nl(dUd@Zrteqs0+)E7s(B-nYu@Bb+k(18&4{bIAS^8IW{F052V(V4@g(J5prRYi=Eh73WGIm^0Hoh}b z)}kn0z6k>n#4`~8+T~j@1{YrO&amvvJrQ%LLmR(7#BNvBuUtchJRmkIGm_-8kJ2Eq z`r-vE7o`?b1#+;(FK7X#IPvrE5=1z-ok)h$R9zaA`L(BVSh3}936vPg@hyFgc(qKS zgN)^UC?R`5>6xxUy}_W%#ld-xpDxbL*3rQFhvd={?#`-sb21Wa@D0>g>{IMs`|>rX z-;r<8PXv8M;$G{RX|ssD|D#*OVgy3bBDRr8>-gkEP=ygY9PPw;)BMa3K5mjkEboMZ zgNvhAyMJlHA>0FdG{}qVD#<+Jr2Z6H_R9PBb%vJ4@rsqj7j@5*J~x%;(evj+!822$ z+jU7H8dAe{1YOdPT$!%0PiKyY+BR^3+AY|g%935(h`L(wnupEq!fYLLB5k`(JNHRT z1e#BYHxk{)XYb;_y*j-#43m+3S3NStC8P@za($#$Z;2l=^({2rNeZZ_3O$sYmP4@? z?V{964B`pZ7Zo$4AaKFYqsZ!Di4laN`%McEO7D{8`4A-Z`lyfO#mt;YU7B;INSHN* zrUe7Bn`_C&)hr~G4W8>SGE7Dn1yEdXzi{xDNSWfFbBv1a0Ol7H@->GSd5jU2p? zppx*k!X|q-4 zMa2HM5oo`sJr*n!bq?9z*xiI6ePVf%1EbZL-~n>#kdDp$RGr`0{g9RIItnkOcUXr# z_hqyg&pQ`GKI4(1vSW*Kb(lPwLw)c=5Cy#hp!<8}p~D1JtYElvE!@3|zgu;SqC#*X zo*_g*=K?>12mBF1upuPCA0ZIR=Qo`;q_Mls-b(g-t*xl;N*2uiBqil6s3BHTN7KODRP=uGlHI4<9_f zs^A@q0?Zb7pjFqq4DIKe4SI+zeh<#z5-;*X@BPi_v){hNgsQtSRtu?t!8v&G(M@wa z?UQhw)}CE{9wI1eCua1bnE9xy0W3KOeQdk~@Mwn!%oF8ba5`VAx|NDGXI`!NSE5-y z04`3FHbix_)t%xXM6l327fUfy#@?rLNu}o=wB-^Kh8R#|i=d1T;ri2LV#fOpIZN^E za_wK~5{k~QnGY^}F%)OtwwsRVzixO|%eV}@P58NQ8og_YD8G3Q)9<<3M9#t*gQAm2 z8UcmXPDFB4%;*4yXm8rQ(q(-T#)sZCR3+r(+$M!)*QKr)dft0B;)%aKzt3ols}5*c zmnY-t-1Jc_wqgY(nEW#%uw;X1*_iLCoyQq<4~x0zHAKFrB%1;1$EE$KHSk6Oh62 zu^-J_gAKm(t{z{Zww3(VAKaYPZmQ(*>wGd8Nf(y=eywOLJ3N3nKvRxRk5X}AKcvUZ z{M?oXaqjaxPnqSc+E~>PL11omCWqbF7Njg?ILKk?Vh32+$We2ClSjM+cqsX73|0&6 zD6KOw3VW}8c+r`Mu#xQ;#@rcKR88Oje2jHdc;fwc!yhpJMqH<#Rul2)BDB@j>pP%fzeXPaMK>>Sj{$=GCE7R}eql zPJLKn^RRG+?UTgLs=PkNL}Oxr8jq>;G5uYBK-W%NshOht)t z##SW8Igmdx2?m6$d4{&XlLAyxiA8($=ass(xS|5sULQgFDLUE<$77<_<3-&$ww zygZG&pPW`W&<^7r^myv_-6h||C8v|5leqIW8bDY6BO>zx_iWvfze#~Pf^<2d_CrGy z=>`9dp1?}F|7!eE)nWCL&=hZJlh|aF?x(X`-y2v#u%vW@3Tb=OGZZvo+^jw9FJu(( zY8s0PwIwdw%ro<0@X#IlFxlGGyJxU7b*~bkCoZ}%(!BH8#chZ|*sYDtvtZ@@`IsQ@ zt;j+;uGlQOCUu31dh&_pIQ(H9Yg}3WH;#B-EK~zlI{K z;*$*Y1nsr&NGWXA8tbtU1%>bRml2CLbbHDB)^FG<)R)$2RMduicgT7kjcRYP(a@0A>G4*pfHfpp#j5yU+>%(jbZFZ8>F9m`66s10|vQ7^9<1s zatMmci~2qU$Ufmf9K3xKLb$cGh53EC4zK>cMwmGnc^4E(;d92h7qheHatrmZO*XG9 z5B#w%fm;KF?)k#NZ8FB}qckeBk_eMcq1BL$0%rwyAvYuKiu1MkdTJa_gBj-d4WMR&B@!D%{5&s2V}Za z%wrgu)oXxn|0@(YFC8-jj8N8b1dVE-c9F)_&zDT9lme2;l9rQ6)D2XPW@-KO47a)! z4ogIev-_9#gXMluQkMMU2k_|LS(9S|ukWZfX0Th3gb~(T&*l)Ey&g!6X3o4+|DoPATB-d?%&{8||HEFMZukO!tlA`*>bM zn@dHEd%ugX9R~i+%V6z4yc%v<1)= z&Fr<;_q+9`>Qt;bZG>1{0%>3Q=UzgCUwN*`uBLnxIb zlbtKX+bgSlLyAj>yCG9)?Fcep;cQbQ@4w^SVI)2z$9q~jQz1Ms!rd~7_5+Pufk#{d zz59OO6W|&tYEQ{S7V1Hwc4rRd-#!=s#{HiMT@=`;wh7^kMl+T4kp{Sz$kvT(swXMrDUxkC9r$bZ7 zp@k*v;mc?05(=`pDPsCF6R+sR5?19`5p0xnNi|OqS2**`VUDcI zD0mk0p(ZM;HhqdlO9zI%vM`YhA$`@n$x7!@`l7_;kuM8O2Yz5qt)76dDW+kEr8COU zn{ecCI;WWC-NWHN{>)ohOfiAC?YL!Twx5-Wyrwo4P3A>5k%LPzx6n`h?q~OR0`Pxr z>8|Q{=pHX5FN>e)XH_{a~koS(J1c!JEKtgKds8hzjbvA{VnBiaa8aOF>C4Y2r>EL~0U#24-{N_w= zkx-=F_cnG&c+9dw3ZCn5dy}(eyuaI}*|RtFWW?};_?q_#a4Zljh=1Ny>Um=rU4E-j z0u3AjIQbi2X1Z{n7ibT6kbGErklx1;HQnS>v2Fb|fBAyBDZ?7=_)4-XVZW z3PJJfZ7Gx*wher%mDj6CI2B68J>?qjI~pd-71VqBgJc4_T^RkMUUt};IF&qD9^Ck4 z(4eDUA)@oUhwWdV8aBwNif{9K35Z=Mk!}l5ABxRlEH+ibgmp_JA_&{hmn^webPilM z4wOYNX;ueUwu1+040QNI0?BPIRZ*P!6@A*S%OowSEpKgaC!@>2BV+WJZuTB~_!q;b z+U09`Bu#6_>kkC&#J7#KPI*BRX1m}FM-{5?aMjfj5gh=u+fK}PpipmPc3 z#MF1^on#S^<%A-GieNHg9oaRGMoE?aXjMZK#kp7bx6&?C?^}XUyHe+Pj09imD?ZvI zDg&ZEM^8U}Zuzu~@)EXSB}Ef5nMgvQ!+F?u@$~k_=jGxoZ3~q@$C%xVy*vaHcf=RV zft`+D?R$Kh7>1s6Q*-3kJ!!YHgU3kp)9mu~PsfNxzKDz$peW4J{{F_i=e*Z?`D-KO zXoO;f0ZKukUUz%Ds={}W?J^xIHAU>gJh-$~eBIjbzZzyE4lxa-Amh2hvGCjN*GX%% zxYV&k?1@~#qUSknzgei;GP6OYagFq16eW=%{G|RCHjbB$&jPWnLk0YKBou$Y$$g!x zLcL=7%)GJu_bZe&cKJjF9P)%)badim2fusUmOtV;FenQ7xc6zFk9OCvLC@6;<7KWC zeo#{czrlpgm~hdOKyjSJRZ(#76zODX8fp4_Te;_J+TeSw=Vf_Xs>y~4R&3baiC>1_ zc{Oi?j6mpn#Y`EC2xGlOdGJLxfD63pQV~%rzjtNsaN>vb0(LlnD8Y!%V{rG|FOu|c z=!vvniX*K%|+d#|jdy)rL+nBq{_4dHfXh|%%;_FlaChueIybsPyunkTlL#r|V^&?MpF5Z_IA2*nhUJFwd zD%Y;)KwX+|3piKy1kBl858arqR}MH^tGSRS^~+E*+E^kR@s*981=V7F(7$=Ly}EvZ zKM=x6_b^|<=AN9T6EYIC0|O5*>}@$OALNp2CX+OKRfNQ9w!2I3Jv;Lj@+!>EUNQdW z{Gi8Nw6MLmv^unwUBixtD?v9{K2jGI`DZ$8L20aR$O(=^-=m*@CU~3h_3cGvcXa#J zP(~NOqfviX>!QzP^o&Zh9IeD+m5;vxn@xQD_^8drCLBpxE&f?o^P12pEHTuE^$o4q z3y~ggW+yJHa68K*pIg(Pmx!^lsQsg%7;L_QDQ4^>VZE-mDwgv+`OUZXNVYtDF!Hs6 z9@hhxNeZUuE(!sJ-v|D+YPqcJ3oYEqGeMGw+ty5Y{e4EFPO4Qxn@xK&h&`au(I(y; z=}2pan#ZmyR+Gs+yW#IV3UmNBXnh2UF;3-!7<*XevPvg=r4ndQWlqlXPE?>jf~7Mb z@&f18SjrE@PlPE}I?Hiz15rRHJg8TvN?T^>3{mM_w8j}@Ju{U@Q8&MrT0OVGQsN-S zQi)pJi>Me~!FM!_vd?gNS=uO;ZYg2=bMgA z35yxR7nkY&=LcVR8@xCA`eFJ!kkrMSg@$aKq~OcTOM&a?_@HB3^KWfesyoH%Oj<6i zqN!^~=alV8E&v^pMFG6xLQ@3>`1(IBNaG!`2!7|Gvo(;OdRoM&Y1!4U4>~CpNHooH zsp^Jj82s+Vkf$d*lrE;JwE+ey61b_|g*Wac&uH92+!}g&n>(EwX(n-Aw{TB}TRQgc za@hnD60a|jam8FdL#w$6a{@x;DQ5s@N^}R*Po1;Y-+oCyNB)!@OiC$(((n=K+t;U$ zAE7he*936o5+!nDMmmu~QT}nE{Bu9>&V_;sx=>c-y@shk*NG5#6GINc2Rc#&|F}|~ zf)Gx`69hk~UXCMtNYg)0Y3Iq6E7(52pWNUTyIdd2YbBZ2z{gAfqA_}5`C_*meP|%8 zjxbtB>p>^CqKTO%D!RbAC}k4IlJ}d+%MU6R_Z!oX*^V8Xo9l>2J|gyW91k-xPAVC$ z(jqOD-F6I8=zhwg*&W37<;k8Ljv)tDWJvo8qoFDCT6-|R+@aeGxmrnK1jeZDb4NFy ziJxjsRlE$QiIaLdQdbH+5c3dqGe0fhKd~%Y3$BH`lvrfGB+rTnNS7Z&bJ|Tz=<>oP zm>+y!p|LBZCq9;mUV8jUZ+`mQ$!Rr_#RgYZK(^(I&xSIsZOiA+i=)Bv+UJYZIxB0Yg4Z<(F-;wGuOnaYDReDo9s4^Su`K5zD@NpSQaIO((DaveZSrEWnPm~S+K3q z;MF>69%yPW{n8&VUA@_$;l-=8x;b(bbP=#s`65DNSxBW3zM$jpCoU-=g$z;pfiRMG z33+vL?de(##|Ob5VC=ug{6b?)&)D&;o)P+tMIgMF*#2LIJNv zp6E({!*@*!?Tym$8@9v)+TTGj$9dO*1^jFoFA>=(Q_}EQlzM!*mXc}lRIK&b+jM#I z!g)++<1F37+dppmcL6uT?%=v2{dmTrAg%Lsw6YPIace+xb47h~)ucjHk|+RXnZDW7 zoA*OnD;_Z8x+ukoda379>F*}4&I=<>a~<}{il8>~;9g!zE8fj$!V3`=W2hHCnSbP2 zFn_?g+GmK*Q=Q}d#R#i@_QteiO4E`nO;N5!3#CP41-auj1JDy5y?;$05MKG-Tj(6W z$zX~O`fwv-f&1QVqzWw5asY<^ysOq;)YNZ7sAg$-;5U)8>H`rKLAeEmo`arNx&61y zl8H|bNp;%te!NEN>lC1P&549T1#XW%C*^b1*)F_N;oBSss-`u)KBp1erReGT^6F+D z$-KN>7*1);6WbFTtuR?5v9#L`*rpsYN!HRr(UbJN=-gr<77PcRVFIE)M#osWJP@(-t6_Qj& zZiBbg-;UP11eP?d>3_pgnU$zToP9MMd z#-Eg8_GRW)MP!Wc%heSemXO`mHVskJ7*kqMG91sclTgF0wmOIQ2+YpeYw7z%>7BHy z9ntVG%&Pl&E}23cPJIO~H4aIdmvAn)y35+x%tg(sH{!!qsg4JG77LzYA+_fv$Z1oY z5W0|$noqD?UB(CLqO;$6oFj?1roSwRARU)*4~jtszYhC#M@HpBVbX{}TLl@jXLL5; z&bQ~)+l1@P5l1pp-ldcXL(d4Ic5H1!6Y0^xK1eGeOO$5^Ij}Pnz^S@ja;g^4|JG)N zxM+629kA~mKxjs}sDH$7)u%f9JX(=^v7`7jf!8Niu>BZuJU)E*xiSFf_tp+y64WLq z+YfjzgM+4J8)kQlYjjX46Jy2%4Y-Q6zHXDao~yNlk93Jky?JDSSe#(q7#crW8#J>x zaG%)os%NT)Rz zn$y=?;&PtXPd+I^(mVWKE!AV>+h~P(le6fd3Ck<_4!S}?&I7%nH~SbmQ}7pOCk73R zjTZv!4fdBtvi=*e5#{qbm*YbGg}K>I*Qh0%8h0L;VJWlVh#U9gemVeH??Zd@^&XTMZvY4EO?kZwhL+5h0lmwAxnh~TN+6#on)JkXI6%uIMQBdyY2R=z>Ha3hu?J& zzV~k;tNtzPgPJ0me9I`ait~f(B7sHSlk$j<_QTeY+_mF1tnn5`DSMWZh+gY+Q``UO z7kYVA7D>!$hK3no#m4RWD&ac`uhj4ARl{h67@SCqEWkqVHSgY`0jld{7Fld7u}yfy z;F)uObNa}Ugc|+qOHEVZ=~=#*?ihcOg?5S~3sDNvGo_wEEyV|^WWh=}xw<&NzVro6 zFT3~?KRx@hdVO=-d_!Ac))4}!AM0A}6pOmWpVYWY_QPD*9E#~_ZAT(|Jpuo!hfv1F zjIDe`yclQ=6cpecuEIA}TI<_$&md?qsTo1RcJ3Cn@QhM_J@59#SFR)?@AVlCe1t^O zVeO6-JQLf7wP#r>5e+p3A_)oLRjA|F0v7?>{2}SdXs*k`dW4#MF0}3gvXpQ40`u^+ z=m$ci_#sB!3Kw0xEHz_r-@GkMvkWq0ny`n7F=E$==>gM%j5Bn@p7zCRW9M4=u3-Y9WHd0c4 zKRhxOS!tTg_T~K=I$n}ha1KO;@Jv>5UG92_QnlaMP)hn&qnjnlYjtUJbAb{|0DcE? z;@B2n%Q5VKJ(iRv2)ngNRz2F|Du8?Eiwb4LXxCr(CaAmMot!Lalp|#+^t^{nt?Mxo zZ;fatkH|%%xvf5!dfrg`d%wbj%xU8EMWm7cuDa8-{>|XQ%FV?_mC&~6nG~trq3S!L z(~G@Zic29H)uBZ_Dh&B>h-y7c?qih`EA-^x=%~{wPDT_AgTHJ##eQYk* zHws=tOGkXeP zTflQk{u1bG(eRkix+j92c4m7x!fRkO&cO}ulSeNa+s(|>Q~KGESOO13ic8znzfNZ=~absFP-1Fv`Of z=!3sjGQGkwGA*;ZNVa9%o(*j@fN)D`(P>R|taR7c(GXaPqulYFp$Ag;(De@KXyjWX zeGPUqKb6w!3gp8qa>D!LkuPv1_WJ4BxJ8(?*uc$=h#?nQoPx^Gu@!N4P<5RiC8|Gr z(W8*rdN}>|@b|HLJ+$Zd!jkLt0PPV+hMrH-{$??@Wywp2N#Zoz2?Jrg?6Z^r;o|5W z%{+t%v6mQd|mOoX1Xk2W>|>q4V!N3zZaBlyeJR0u*9(2 zjGR(HTqrP!?Q5Ij^j{h66eV2}5ysLI55?!y0Qzha7Gd_Xw9bV3Xc7-;c=ftY zmPpZFQt8bMP<})rcq4658vdj{=_BX{rALJhIPr42`g!rU7S1ostbw;C*J{#TyVKsR zn)R9iD>s`<>(4xNZr`Xc%9fX0B~KmMs__^pbrO#$YG91^RO(jgp3nUr!N7YM)$C1E za8@@sCpjV|1qbAvJ&4Zol4X@3|y6Sgx{>$Hz2p5+92}uLG3~;`&h)HfeEd7PXgF&|9uFIr~y2*g29@^94!LOeO z4<`#KRHnTr%*;)4id$;Ywod4T4wv9SPJobugu5=T1uP#M?VzBKW4R5^`d~R#Tr&G8 zj#a5Xo?|KM=sXBj?rGPh^sT(MNQy5-f2C-WWb#Ls0#I1{KwTs?uhFM>zTD^3dqvi){Cm86wS1)?#{F4J{yFAkY(hhCPUN&C9<-%J5xZZy{_> zl^@Zr+;4PH0HHGxBSHV; z^oI)p!h#zm>2+6>M&Qkf{(c_#)<#%yPVZ5IU#4i7v)Xuxe&q$}`H!|f8ad#-=aVb0 z7a0^Fm#iXz zkT&_Y6nU1ck|v!GQEoqnZFbT-ohUOasN{+oXs`l{<0D^D88zNBdUxs4B+Q{AcaVKL{{%Et#m*DbYzrHn@oS_P9T$!Aq1TT zjVM_M_||jht*dA4GXtW+yz$Ao<0=`=+x=|l9$rgEQ7vP(H)lN}Zu06gZJD+bn?&!s z$3^(1d6N@{U1*+eoFM}~T3`DVy`tV)P&8tSDrVfQcs}myFWcQj`luz%rFFrOisv`X z)-pZEdZ^Es#$*YfO-JSn+d?Q)Z%z@X4(H}d!6+2)vMpRa^1rg^AL;FGqACv!r;x`( z@V&V=ysSgoEI!dw3bm`8kV3Lw%r_;Xjb-W(NHVsQTTQ5gXhi?j63Kfz>9CvOMG>|f9u*O|H@Fof?f&)Q~pxduCM9S!$^YO;or@UNlvr=Xxv3W1F zX8RM%ss5(A(jwTtKwGyBO~73+LBMduq_kujbS2-}zv74^`V?gx3$ji6XE~*PUzBfaf07g!XNGZrSS^7JFagB%?WpjC5k|W<|JKf<3d$-H`Gt; z&V=CayM2%A7lQG-PMvN{my`S$lp~m3#N^Nyv(nwy` zvs}RRZGaSkgl*Wqp{*ME+Yu&Y46=@}M)bCx>skXe0n(dlBMH~$pqH9FjNJU~G|1O{ zM<+hKU18q?3bd_+Q3c#K^*dB7!0$I71*?H11Abi9IB zSS`DKtOvv|aYTiUu4)5!()w%Lr!!sr#S-LEh*4_uUf88~(8r(E?E5M(mA=vP=V}sM zbU7qGgR+oi&Bm_&0*`i`4{p+p>yPkVQl2kkTQ%-Tk|b!Qs-?MVNu1?;C_=q7r;A8- zs9|vz5aPk$hS^P&-1Fi&0xD2!aYZ;GijvCeEn&gNN$DdoY43gJstBu_#$IewTt~+y zFb0jt6D!a;-XD-VxB6nNrOMMXHfX=xD+Ota}ClnRc6lW4Owgf zW!1S>Alp2u6ltO78g5Sb%kX?h7}LS_j;(sy9=wi%ziG#ED7|0z?fzRoU$3{+ug@2wsd?wrP;u?F6pN^kMRuWMgwZeIW9#j|dBE1JUN8O_s+4A3XF^$YwIKAb>wtWaCN z8MisrU>xZHKD}J@H>_bY9#T|^*F*jioX<5s)Bw6_lAFC>0~V^K=IDv%CclFlLS%|G zw%OIoq@G*G$M_mqvYjbq=o)o4>7#R~?v@M<*3Q3}eC=RkiG}519y{}<{a{d&)^#rO zF|Tw~afFh{htS;TPl~=QYYe)Z>hT-Q!kFA2r5?`sRFeFBnrD53fs*#kCXO^3w*p%u z_$1uAMgGlGqpLkL=S^*`qm&jBR(4|?KWZuUaa@OsvU>zoLD~8eP6_##(EFU=KRd|i zy@tCy?f~y=9@!K;yKf(oVjSIc;fd+QaeaC#S5Qc76HK`u#5}M3>X(RoT6u}dj2x&F z!0Xxo@Yd-2j`XK$!pDxok|#o~lzs+3B4%ylyT#h6WS5g+pGc11zp?xpu%V)pd9!51 zsh8@Dl>@oh7@-a4V z_)aReTTbBQ2hIT@RjyN#1%HPVDu4nqk{C=HS{v4LVQh0~asu)bMhX4e$zLJXs_XaI4b2wYmd-*8(|h5F2wSO~&Z6|LLZ* zsQiMz5B!e(!p;2iWRFFLUq!^ zGb*3?eHP^@fvUS;(A$5X>Bf7gebHHh>}ZBZE7RZ}_2w*&RlmM#==JR8=gQ{HiKtAB z$Qj6+rh3;VRd5nFWHn8%IPG-v`H(*v>oh{%>qK9zs`CqzEWrsdxXlIicnE3n^H;)!!Qh^ z3;+PY;@`dO`rf_!pBSRBdcx9I!CIZXi>JE{C)a)h%{CBNUb`}uLq^8FeE z@Np%%VSE4noEQJ*7OXH_&0~?OdT0X)l~8eWdc7Xi^egM$))N!93e3asxjF$B01Uhf zfCZGl0ANr6-~b8yfC0$>02qK#SYaDrfJQqEHlP5N0MG!w$NYcJf2sg^84p*%S85vn znE%iDXA|JO@n~zqS6Uk5rv^}LF)YI%n+^Z~5&!@I0Pf1TEPak-{>b@tVF>^Lm2hVO zRj2R&*I(+vZ$ADZ^JB7Z%@@1OWIb0FXO}YFPS5AJvw#i$cMmfzzpWjp1cs)kK3|<2 z1{gNNSM5WcK?C$qi{6Ooxu&ajeANd|L30{sM6$ES&oskpKVy00000 z007`{;zcQK?!LZ1Z{-B2nr0Jv^nk$t0Ga>*3KsNw005u>4(1K> A;Q#;t literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/search_updated.ogg b/src/sounds/FreakyBlue/search_updated.ogg new file mode 100644 index 0000000000000000000000000000000000000000..c7abcb1fe7649d549d16b9ad1298a971958bbd5b GIT binary patch literal 5500 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 literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/tweet_received.ogg b/src/sounds/FreakyBlue/tweet_received.ogg new file mode 100644 index 0000000000000000000000000000000000000000..5db020cabb95e25130bfd1e145a23be900c1ca39 GIT binary patch literal 10425 zcmeHtc{tQx+xVHm7z_;tGeToGqR zXG;q$6ctkG+vfcYJjeu8ia}D%h;Ex@}yB@PUVUKsx($Lh@AZihv2^(Xe$K zr22&wZA26YwbX~H+P2#aXm-XUcC`I8XXjIXBPabv(*4Hl*E;ro^m@O^f30hz<6$6I zUN2dmkSs??ex;kt9Trgr2Mi8Na#dnp=_b53OHlMq_B@k*I6A91GwW^1RsOXlkYu?z zZv&=I$IPBiID95~t02_e^6~P!E5>g z5h9!8neM|xZcB7sV+#`ki_JssL9Jtg7DVN}#%B#=E%)jPWxdHQ!CV=B7;Se&xuJx0 zC9qF#Hv32)?p1bRAE7(5EjM!@E3nTTl~uUjJ0o(1x4dKs8pF+I3$^21p>^IvS-d3^ z4$xluK-Nrg0|x3vr(RL6Yp`2~w+1w@ujF8M-$T7=Xh-yuE4~eU&QR|!M)t>EKF%0~ z_fNyOramM{Zm#3l_M8y>g#?5fMz%;cMJDL7P)e?5C!s(lKEcGIB$PtMDHjHXtWk~! zAPoI?EtZYIx<$#}$PVCFEiOooQSDZ#d8E~&#%ao0`gY7$wxIW*G;4<})U-$J-Q(+K zt_D`z?uz-w|FQnlav+0&sxei;_+KoCttqt!GM;KR z>*aMC4F;jpA;?(b?*agTortpJ93=`iRP_<6_6Sv-W@kYApUMcwv>?W&0e}ObrS|nu zK!1F$nW|~sXtK>4g*GY4nA{NiKtVBLlzFVI8^5fQX1dUhqU^_#wgg#`2dm0pbrqeB&|g8baA3!7Gikay>qxh`UFi7 z1@n=K#0^;p>rj<{28RIe4NYVrY`D!h6GevqupwyIfR$)I(k9Fsu0I^6LdLje{r8Mz z0>Fgv0wxgcCjIlB@*Tth&`oxyL-B=fvIh(>J?Qz6w@)vOmizC){O6XXV8`)s2ZE3W$S6=C}(fKM_MWv;x5TgDy7e%8vdu9C@Zrq~Z z|7W$c#MRj|`6@9*C8ZStHW*_l@PQ8SF&+pgG*mj*NaBn^L(URW>mV!U;eQsTIWxUW zOG_bt1Gj?~jGgen<#M)EZ0-SLC!EL9Jm`oiXtujr4H5(!2Jmd;j;`Wr7vvJ5I!l~Bk}R#i-+8k!XrP_-=73)87QvT4Z{CHMj= zmB0g-(z?*c0T~`q#tl9}(VjcUY0oNIH}2+P@uEd;vEYb zQ!ZpYg2qy)J2=6QIiWBJS^~w^M#-{-1KipL8$@k*0w^V==RNB$T!Z`tMY3Y1( zJ%_DhCtN2XCPG|dLo9<>*vWC90-5{;T@Kn0C+r+P54EsC)(8p$6)S%$9K;*T()&8+(918P3Jt8zy*DJ{uFyBQ9Aj z0+76rL=B@f-zq@yTsYc&z}&2`4!b!F8%nXTD6eDb&q{?+9evc>U+c}{3m!YhX)29D zloY1B#a7)BC#w?bM%~J~uxV5u^?I!DhqmHW3ro#*tglA<3}^AEW31+m+ai{6n(dBZ z!tHJ1?qzjja&f}dZ9|UbbqM#ks)o98pWX(nd|ZWkoxN`_4`Au*0+QmmZi~ebecJ0d zbV|UOPoYZ~q$KVfU4TT6ETP8fK`#bfobpns)HUsaqCpCoP*@PBIkPswiBG)=9*^`m z6Hug@Iz*k%4up2B1+I#Pk0E;2C#V({>JTUu2oz@>)c)B4J(d)v{@DR-gPz0D5RSEV z1*!jtfuTop^}1B5%KAiVvLgg$4HVQsh^Q8SCa4yc5Ihc!wb`}S?$0Bk@G)oG`mCcb z#9*rG`nuZ3j=m81^>yCm5F!gg+B$gX*bU|aB&KkhxZ4ffewko-ew(#eWqffQ_04KT8TS8*MgDKE>XP$&KM3T|5Tq<}NukzEy){R$>hl&QpSV*tm=ePhA z_}|F|6l*aU@#{Agj}UMbVa4qmgUPNaEs7oP z3K9=su(bed?HTqbFG1R}HVZg;362Mw@_z7G-oV6k&#L76Rh&NUqVcNqDr%Kzu+yKq zs{Gfgq24YxhU4l+ZeyF@WLb*HZ&AgR;^$};-qv7MtUxrX`I|NE_JOBkGI_7v7h;#c zw0FFz_IlGdd+^0BnZ3o;$3GMwTZC~NpZVE?x_=j8YN^)ofMA%*c;PpZmaUt*VCp{H z9;d5Hx8uFAKj(;{k=#k;3QBSlFCO{qwq&T%b zo*_xq_;i{UbTgfW@huuUiYP^!ofU%tRW=gG+kU#>vmt$y1gBqH!K((V^}tK7YGDBA z#)1L!{%e~^Qcf`0gem27FbsH9_7%j3pb*-%H;J79510;{31C=`sD&IbvATU8S3j>D8K?>f+Myj2vr-9@mq4pSF zIXZB~trtals3=amtq%FtriGl#jV*?ag97$f&K#Nb%9TF)_ z{-)bKCf7;x3+IpUgdyrLE*)}7xHlK{7?CmZV;-ZJ82a*3T;`asq)+%^hE2TPXsR)d z4=f&l0vID3Bd7=hc5#=6n$sXz35}D3vIHTo1mBE*{3(K#<+AX_IoV(x`q(~MHUZJ=tSe#+~aP! zXp__VMLW3T)#Drh3G$?X)Px@iaV@PLJJM1oyEJ0&5PCXe&ZdmfFjk<`Ulg_Pq+bz_ zK_vaI*PbhcpzAt#x5hsw%u^?>_qxetPEa+YRhT^6Js8?rt&C zY25}}_r~+3zQOoD5K9Q9H$33q&M%W;Yp668EQK%{aGV*U;mC` zV)#VI@{(o8u{zmafC-oukEqvY)U2|%1V#w2hRr!$ewymt2JnWed#^NfYk9)6ZIzV{ z7kjpP>Q1%#@PwLpi4Q-dW=t#`?(U$MSnMfMY5T3!b9(#3j@F~LHx-P!_|DbDdungC z?GkSpeqSW^`&y+--qP2vlZP@+JUfGKjmo>in*MTP_1CQ!xWd&pUpjuxN3Dtu&b)c{ z%U$NVRyE7tZwfp3ZQ%2ikb-ZqNXDp?iA>_cxl~e2iwbo=rXv_rJS;rDg#kOk*m1Q! zQ52T-BzkGaqdEan|I+yG0qFaN0u9s))zZB=YsN)zpta7ULPn zj(2XuSi7_KLzk1`sVH}!6dqbR{iVdway4!&yGqU(Jc|46<#-(is-<-K zNrO@V8^UG@Wo!uFkd3EsS!*z1rjFou!>`P}uwZ6%#%0y5$|9F`+9zaXm^QrVA>cJM zmOkC=?umZHJ8< zHg?);oR8vU0b+`aNfs5ty0%cddjK$1=zDjJAj)Xgo83Zr(&a?RFKH4Rol4;Zw1{aO z0}6IgaUN1GfEl3lTYOid49*zg?r>~lt|L53zz5q|eSM=DP)n&x@$f6-aS?=b9Q^@U z?)V@Ag(OOtA-D73P1{(qXdug_MJhvsw={m}#zaC#cLg+XLxGE%LuwI93J==GVaOXu zZge~_LLNqgKm#3gr%4xy>j~_u%CFNOt%Vv!kQo>*V1&rZr2Beymt|%GpBm^2;U&o8=P%_i zZ8{p#U%^-+o5V!E9PL%O1)mZ~+8<(pWNctc-ic4H{aAKhxk3ELk)tnZ_=zI;wuNZ1 zGD(MeuL)g4xjl8Bjm^m9ZKP03GvP22QVoP9{*I;H95E{pTuR6hb8{DeuU&eipG2_Y z#|s>KB-yLAT!HgUm4;Ww?^oMII?g+&ow@C#P`$vt&rz|`d`3LFcMS*THoOj-{K#vC z6RlVKWG;r!MU~^@BwXt5;klXdVoqDDQiFqiM+04zc2+JcjxS0*I?r1kiuy!C1Uw8( zrhsm4DUz_MkWiRG$L_3e>4T9|nYBM6gPQVGHmn4~uzf6)f}!CFpOxX^qpnByxzxV8 zp?IJyQik-DxM_mNKeYDfcbjysAcIpEZ@+auSmreNY*X_ulkk|g`@1dl1Y2+^hFbM) zb;ty6A&tBiizb7z$j^D8NWC=Gk%%gT*(q?{qyLhR`xIC|bldlAgiX_7f?>(>$*3SWzt1@6Md`v15ZUoxG2A zBlEZt-%V|IbiQ$NjOTfPS1sSmpsc3{ob;d=J_fo-KfgG$xv$}Mcwkp2FcNugBT6~t z5wty-$%=>xS*%zQ&RY0<<;8CA7r(w|y}EI}OYW7j-uKDj_d#DWi{_v0ughxNP}x5> zUlDke=HK@rl+x~D_2Wp~qfGlmqPb#$)G_~?qR?b1HE{VM24rrm=B-J?ar7el`e7+LQQd-I7V=tiDf#p z(jSV;$G}cL`Pua2)N}mq$vgC%?^<}snPc%!H$y)cxpxAgC|5Mnoe%q|vDWIG6jG%R z@XItd9e56hhw5+rRetEy!G&Fa8QN`34Y)man~BGN&GbDPC^=or0} zU#|v8;-J+(g5(f;tGX!cY67N>M#zVL(=+pV;^U(^FTR~;z=50H=tMz7DcUl_ zY1UzY93Q{**~iZGa_8+zpKG{nPCo9z1avO{s>JU1H+R*VoP8HbBDrMh0H*MabS@ak z-4Z%D?wtpkm-4TA8@n2AuaV=s#~2Z(PdTLX>;j}3Z~%41kW!@@y{yNgwF%h8Obhozq!;8@6xd)u}4#6Be#0GpnRjRWin3 zl0AO#`mZwQItLGYo|ln=h_E3O4dRV?tsFKuqwCJf zpXT39YuiKWfx9pfY&^KhF057=+B1MKx-~6zEo1SwKtX}rn@BUl!pZNd{BBzWdgXhZ zl735b%d+8d_ck>f>DW$e8$h+U>UG}ZNwnlr_)H_=gL(a=NC+VyW$@y>*Rt)|QR0Ok zPIJdJF&0l@T3ce6NA_JgarVru8tV>86ImBMQs2|^Yt3)Rx$c~4n)WvdWWz9q$DV(9 zDN(gJR`|B>OC%N3sQ;d~Q%gb1EV}HcT+;ra0py1n`KvqGvM!p|XC6!EF&?61^ZUZc ziC2@3NR=7Weryx<38J?;rsjx#rl&o~*|x{zQDgU(7S?c#fy*Vw??Ks{xj!XMDjC?X zu!?rH#wuaIhMVLNQmTflgin1gP{ppMjOG`4jK;NHE%SX}KTK6dN;X{|sK3e$pP|O} zONC_WB`sX```*y5Nf&Lp^dh+C>J>LQ8@6Z0LAjx-!M3-SsH4d0%fz@nWH>d=c$vBN z&y3LDN7RH`4|AUr+a_P*PfP7_)mO#DPbOQjd@?`0FZE`?(ZW05D{YlDUedh^)Dpb! zb-u_iIz+5K;++5eHl>%BAi@C5-oY8t9bL6%?j6?qhwG+WH~lpE*=Qh3yy-x{WOwn_ zwaBcKJ~zLH=>*)Ia1j1PtJ1N)qVFCy?%-oDDjZFo+9t{C*lqB{c~?o|Mj}HuwlF7?Xz=0mJiw#_A^THXnOzCSF57b7 zw~-@i-RON(waw`~l%vb{>P%U#k#Byq4wTKTyt&JD`fM`I+tUxc1xz-t#=sqt+$k>I z^#lVqu!O7^Dx3rZ=iD!h#kL5oBA}dt>gwiI#Z^o*?01PlcLq?HZJE7cw~ujdJix77 zjM_BzNs-z0b;*`00uKHVSmc1f|Uy|&?B;N$aCUl-ofUV~SiK14Q z>cXtr?#xqB%JqrFt|TFd&A0$YjZFa6|jpiMxK5$-zyi8 ztFr<7iUkK=TlScM#5xJUK2AC)%ZJkizp>6kmJ#Q37ndGt$=$tEbL@- z{>WUT`a2@gKD-ifS8w2G+vG(^8Fj((;tqPfuYgxVQG545l6koj^rsDCU1?!gc3*Tv z`c~f*Sg2pANli+5rtB2W4pVOJ4l^3jD1`nB^76%vOI1WGO~U4MaV0tWDL8|f9NYt< z6q|rxahwijKifr`*M+z9sQIl7=KNrSb+4N%34|Em1u1i6uDPaK{ek2&BPtg6@@DPRtyE>mv(z3R^T;A>!t@mSk!Prq=tMkAd zEn+8w@W#ZxmM3xMforo0Z@WfMX{A)9K3uJP5 z&Y^zf)3gi33&{#SO-BSVA&pC?Kb1Of7C!vg;)_Vh^PuY&C3*||-$q^h(eqlydauOJ zQxbP8LX?f2g0vcc_YSDI`Ylm&lW^>AR^Iw~)9~{`H=>HurZ1+(4 zpkU=Wu%p2;)odGEz|t8)(*`%{oDw1p(0MasqV#myGV=XYu`LYsqK z;1@f8{@TGEHF|&gMoC+?_DxG`&7{8qU+{EJ?4DlPw@-fN;Nex<%?Hp;$L~=$@`2tu zDy`~>8#clfD-7k{T>O*0m0x;B{j5KT1c?{0L3$N%5W^@Ov#2Nn%<{_~Kq<+X6yiaG zuJZZ1Oxs>HO?HqcRe7m-wC~jqocQ^@GtT{XD-`RhUwM*Kf8Cc@sLkm(IolrMedxjY zDd^8&?7lheE}s;GZ2}Kn0&;JOJw6sn+uu97(>Yt9k@BET(fVxv>J!YF7N_I76PHbg z-+va_@zE*g9Lbr?&Xy{Q=Mq&pVP;^XWtmJZiKM*z&Z__NvWg&{spqQ}75-xB#ZEqD zK#wcyNehw?P<08jc~NV@bzW@hsbi!L+c?#wh3f=f00|%-UHYEI^R{UFgI_JtxSY_( zDE~)?GR(o;Qp|(ZguC@wS+2XS;UkO*i?p|u(XI@PJE*+b3z(E@NnucIeDcGGNA?ln z-Yz}a$~Jt9TeNl5(zgkrfKeV3&Pp*^*~RK^E`O8eyy2z;=ymq_wIwsY|MC|@?PFQ(yc-mOi*kurzBR8jvUsf1%|xu+>fD%}roof#8oD?4uZ};{NGK_& zX!c37&uqA;QR=MR@wCLd?TSex@fGFES@-OkGL;*sor>k68xmgbMv7U2m0q|PsRTI$Z)6#6b@ZYCUc zzvJhny}M~l1sS%4@ zva~yE5Tb`piQ7`QGPg9U>0c`{=NarofVQ%R{vT@XH81YU!Vo^Y@1FTKwh?FbK>1l< za&Y&|%Fo%;y9E1rdhx8IA1(wuI5y#Sd+uP}y|VsYbOQ<>W#HJp4OWB1E2kz7F3eIw p1-Y=OJNvk#-wkrB^%osQfB|O&4>&Fe5K;h$vG@TC11Lb~zW{zyxF`Ss literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/tweet_send.ogg b/src/sounds/FreakyBlue/tweet_send.ogg new file mode 100644 index 0000000000000000000000000000000000000000..5bf1b9b8c9738a0c68bf65cb678bb14c6be80a5b GIT binary patch literal 10388 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 literal 0 HcmV?d00001 diff --git a/src/sounds/FreakyBlue/tweet_timeline.ogg b/src/sounds/FreakyBlue/tweet_timeline.ogg new file mode 100644 index 0000000000000000000000000000000000000000..8e268dec2f4574be490e73870f64fcecdca399da GIT binary patch literal 11118 zcmeHtcTm&8w(y3~n{+}ELN5XmI*1UefYi_gMS2%Oii)CyDqW-r0qGq9ML|Jd9Yjz;MEHII@11+^yqWiAzW3KR-_B;Udv^DnJ#F{wCPuEVrho|iP4d>q71uO5 zQWzKPy0@=`yB`5WgnjjQ$amOLNGS$-SiAn z-~Otjv#)`-ld!3&mb3^`*umb<*$L@=6>0Bn7GQzAC?$zPNyPZ@ zzyLV_SYCa^TkJrhIRJD39yWQEi^XYa-={cs`QH0vu<8uSIqUxdAMq>&4?QRI> zhm>(*27nk~MaXf!GwLgiJ6voF^d{`09fVp$v}6if#M#tw>s6lQN%fVpi(KS1RwNMb z08P#l&VD@?)YiNCk@ z+h`=4^82c`iwvhzhmicEIN}2dtpx%)9RUfLkMSL|g#=gv=eiG=x}S3P4s!N>m=;%qb+y>m&%cC%FWsxklqkGjK~~*_1~^Ak7jJRs&{k z#w^}Ua1Bg4Dsd(5xZ?k5M!R*20yHGcj%%F9`J695*^z`h0i;tjhQGsC?3|xWhoAB} zf6Wem{jLm%K?9=l=8?Do+;*=bE$&Nh8B^wPOSD<0P<>f?reB}p zV%C*Dwz(|7K9P4BZMhi(IKMt!YFyEAEs!CTtg>tfg0U@T(YCYML*v|saAaj4FF8>sfnDQlybf&hc){w*ku z%m)qW&He+T{ZT0LP6DX|6HhTuGME2-+T>hRr$`}Je1f)KS(q_~O{mB}^oVmL0O551 zYQ;Q}2!|-1k}O|J(bB@C7}0ma&w8Y~#R$dY);^`xC#`fE)Trk}dSbGp7LM_?3)y~E ziaMS}wePJ@6+s3rQgrN5kW!IOiE5;5NB$9+?hAQEZYbryDsGd%pZ?_FzvTXJoRBde z+w+kzn_FJ@vU(UW@glTttF(X1$E-t~0D?Omxnqh$9I7KL%ut=; zSiyfi=iD`>jt87w5BSg5@EZ(?^=`=+ZOaTYYC+~=%4{>p95Bq{Y$|6nEbBaM?mT+k z`D2~)#TR$1>h4VM{7W7S_*c(?+=f>%VNU1YJtyI_LIR&|62DcF;F0H)Wn?dx<^PB0 z^a`CZ{Li_)c0 zD#nPAijzZjOAhkb0Br}1znh5vJRH&^A?Xf--fhsmecS@*AsPIZ40>DnZ~Oo1i{Ah) z(@)5Mu|R~3m|##oZsWI<@r-Bjj9UL!ME*|Z{0~|XM-ZWY#=o>6jvzw+OSJws*8Bf) z{68%L$n0RW(1%4mkLMH-Rsja?@v9{vbq8dyPF4dlZtVJuNGlc@8XW2H@$pWO26_wX zkfF&ZPn0=D|)gNX>M zJ-_pT*!f}22hQ%e#SB52-feyQ{KddxivDegZ}*t|aw6z>fy7Qi5WAe4o*1pMTc;jC z@&o*=LfV(W{H4HrH(dFkue4q_{j08dofy!4J&;%F+vSE!nMp?B=^mkCI z4c5K9yc{}jh|Hh?V*$==xr8ZIr@FydfX!H{vlZzK)cdYR3{nIh29S7irLNK%at?!+ zn}CVEh8ILZwh(8phbp3WWKgHfv)5PG;c>!XL`8}y)kOtTF={$Rg%~M4@uD;g2~TR0 zUKz3wgApMC*pmv-=K^^|o%kuCNQjV>@Q}zL5q1)eZ!szWjC=qPePSOFo}B*_4;^$wg!3{SMJ@ux zT%E{5I&~e9h{S(4e;C#b0?>I@LIIU0jU!Y*HHO8gx$m9*47xbwrC=~e)&oU@#_A$Pg>k5bqYgrR z>Mr8kqe$q0BGr^3%xab&G~+066(|~k_&M&t=;?_=sDvR@gmF;$cLvlfD@yr01DXc4 zBj_L+N8<`p{t*L1OE>Ag0X4qe%fLl3xb~B1j@>>3Pv{MDV%KL!fd6J78$-~RXFCZxN_t+r?+)#K(Nk>oL%X#iz2boS(C%JBP z7lj@GNNBr;5;+pUU`GMgQ5*L6T>>w6)C&mj5`+`1P%hM;Qx5&}*CFMh$f4+=?4iJ+ z;2|$mJgWQT0QAJcQLX5wA<}^I2 zaG!M@jp8wSZUqxX#C;`o|E#9+}9h-qRV_JSM6#XGJ( zGWASQ4Gg*xE9>6%Nok5-wjs&eg!wr$iHQk#mTx~|)!vz9>b{DyViy$^<-oJ+_BhBR zkeV@hCM9ryP$z=>MqZn|P{Ywr+|@?z^zi|GXFRiBnmY#|PVYp|zeP}Y&UBIwy@}DD z<1r8h^MM@~g+lREfiNkaH%uSihQx`%$vHa1PcJpxaF@|&kpbw1XIb1cOjv4SxGq5C zw!ml|xmah>u`Z*_Tor&Kq^DF! zK%C1SC6GiYi8GsNLuzh)VwNh-zLX>tE58rrzI%4AOa-diySEp3`+{&fOT^OohS~7SwSK!puld4 zt>!wll%TdShorvmBRcG6N*BG!cy6j%c@|)K!|+7Eg2>^nub%9o83{VX1?{2)!&zgk4r+wKLKj+jkLF?E3h za2vexjVA<@KFIv$@Mv&^7~tVLK?6R%tKqj(pePF9-_X7i6lHTvTE7*o#8vlT$!p~X zBVs*K4Yt|is7IxfPemv0SVva~V|I@qN2n7^@N-(llgt!XFW?ARg+_%qQ8>w8zNS~5)fVHw}Sa!;hU|8pUC9C&BmWbnh~;Ksj~cbDx)%obR3~`)flS_=jwd@ULzdI{#0A~ zd3sTPTrTPS{rZv$@=%pzbV{sH!s4DwDrKIX~Pq)Ks=fS9QfUt05BBfrK= zYJ`feOjZ)l+coQ)FGGOQ#P)beSLmc^YNCY)Na|ds?S1Q z=jd4^($ep)|Jsh#+~jp5)u&D5fup|zmCm55il`7~3QJc#)NM*Psm>sYSRJg9x^~p} zw{A7fJH3L{eRt(SaAIkTS({az6G9Lbhk&D}!UD@gS-_Wb%^z>sRy9kBhEUI#1a>%1 z9qxWq-3tvWND5FAq~MEBZEH&$(?o$ee6KN0q#8w#jYk!Cc2SSDhF56W9?I5%jo+TF zqgkutBiYoMmr=dO^Re2aUw5C_3jBOn41-B7B}h36pT#D@eUnl(Z+mL0th~7-ITmNh zbU?dZQq1*)#lqv~r(eY{WEThRC9^!QvcEg!kjt1KhxTb<-3Ws%T;edT)9Tp0Z$vES z&~C5hr{jKYF1oyewLs{zRd59`4D4>_zOu^$xF^3Jj=}Fd%z!o{r$fv)SB*E4$&XK_9(f|t8(?Y1_)yxT zrDj(gATwg(0l+$@=vRYSw$HZ8)lK~uLfB^|&50D%;oW~8HyOWp^1{L4*6xGgTRzSz z-7N9koWFgD?cw1Ag80YNCxG285xsOh0Y$%`!PcH1HKa1)7h-t~R|H*R_FacJNPOh= z*Yh4NTwIw&@>b7V#PUHGAKWe_;s_b=6@~Vt3mVP(%^Wqkse5%WU4Q42Lv%XfcZRxLT5-cH|Q=&GH)Z1PiKVgg%~r zMiVf=*+6#Z{_XXAA`%$JNrOg*cYDl{i48$V!c$|3rtzWRTxe7|*xujBY4Qe}$~3jb zZ~4>Wlc#CrWw*0A5R1izm%yp!|^U|rvS-C+1= z6MaI~+{T#MXq}_Z@AH#tU0wdEnp368JUCj`t9;7!#pxEoGp z1tr-&`KI)X9I&^0PKTf!{3b}4lbsK~S`KMdIm~5pCs)6?6w>;uDKNJckwt;8z#1>G z=^Epz>|?mjZ?f;X`z}<5=t+C_%SapOT1$5+PRb?soMAc<&GyN|qy5*Ld8WnQjcdL7 z&Lx*`80_e4x|mK4unRcVPOSfW_gg0JrGV{H$W)+j@qthOYLc}1{$VzP&+0OD_pAS0 zf!E%-!PPKxv5T@7in}DKTcKMx;1av><5}uNX4UT&R$|b5_r%AA&C~=pP8-@{=H4$m z@(+jOTbVB^oI3M$KfW9gu`$8<7dc*Ss=P8b_F-~PZDRJ9=^`6+1UPNU!>)!`y>BLd zyy9VN7}Iw(y%ql+;~6lV41Yh|XkoeSA_!NMZq|ztQ>sApZGTrnpPlQR%X^XjO=Dp> ztX2-;aLU<+nR6Yjzf!8{Tj^J6P2CYnbwt zq253JBMM`d!~-o_zXd2xvc86Egue5dF8t1T-Pflo1{k~LJzXMZ6%*4UsRvH4!>A!o z`;o3m_L32yS7;3P?c8rN$|qTP+lIur`L-PUv|Cht8;Qkut!H?V@-U*`9t=*ruMiem~_SZLBO{Q);ERoXMZf3$5 zd8J=7HP# zq0y4e`lSDnsUmL3jCNFUYTlVdfFZt_)~a%sz|vUbl;(|7M%FzXNB zk-#ozvVDDQlZyQ2J`g*$TsQ%?VByZX9re)zq)((&u@KfsxuO5z}3|b z7h;*#n)@=zqxPE#>-)c(CvVF&jp+}o{+Y|&)RVM z6WTS7H&EZ9Z2M~DC6_t2G%@B!vOfMhaJzJ`dqB1Ci&=mB#XH)vWT&-DrVLIBKCY;# zDEaMjEBW?>$$Pk|-%GOs74}>lakfjb4udbdRB>c4+VSSjlLBeO9$}A3@o+h%6V(n2 zQ9rhC^afKeZ}(VSVX#acdYEJP!FDS!=GWq)(_lK!oJ#id(F7{~?WT68C49Ae`Ec}4 z@Lo0ueQy#U0yrrl>D*q3VDbI(<=?Mh!g?Z3F>p!I$1 z#9Yva>{g;LQIA$8Od0=F%@>P>`H#E(`5ZMh;=`4>aXHh}x(k$dGM&QyphBYO=w{JTJ+8xu{u<@aQ#to=+yq*>` z(c-Ie7mg{VhwSKJB_A%O(}2{BV^5UFpLlwG7|w8SuV-xQ^YkYnd{7v74ZQw>oF(%` z>P!S16keiBneSEjzG}*{+5e*De&zXfo*O#`%@gC31~K$>+#fE+SZkc82!iYA;10Va zfCVd(kwxSBrPC5Gw|z=4liuLE#aiw8WAkHjmHpoPZoguYgXhg+KbBR6++8A93O}CN zmQJNMUB2DeW1jk3+uZ-q`&b<>a}2ejN=k+d@F^A}$``$O(! zX-I~WWwsa=#(8fw>YMGQ>~ot$@V>~X;8nQU}Hb4@22_nk~~ zfCyXZ8;s)#S?~ZU``mM<$nIa@&%UVX&SpS;A=?q5Or?IAp-myJ@0$7pluAcXj?D-@ zm0G4qN;|%jb32e*i$p*7=@40qw0E`RDF%FE~lGJ>B1qc9MZ+!PScpNPAYEtE_G;HSdNev$E4Vi&% zmG1*0^NMdjcYXPq(~}=2@3r=Oe5XJT?e>tJl=7}$&CZy_fc1KIc|pN%N$LA@OqZ@3 z-8gwS^Hj*0iBwPcNKYQ>rAJAz#LoojY7tk#9{<=ACxd^u3|ad251e&RT~S#rT6=zL zXjeWQ>#?%hH*R)6M_S{PZwNa-a{!I@OsYXa5X_g=OM5jlNW z4%!#?`hA{lAon@3y#RAmk{^v0cu0D=V_&htEks8+#7aB>QOF z`(oVbT~dI5N=kS@|B&L8VgkKa75y=obHBPf7cBu39;0c*?5h50=vJhe?YdsrGk}Wd z9<1Q**!61N;(l>m(Om?|Zx^rUZK+V<(_FUDadm@yk8$9^TeD%=#9#VlGv+a~YkgbJ zaq=0H`f{lkcCZCkQ&g;aP3Nz@VR|C;wI}ah_9Nq8K1MO^-Y%mfUb$IqsCCYUS&=2^ zf@3=ID0O5!*(|^>_kdl&fEMoh{nw9N(-nC-L)nTg0XCg!^#xv764lT*+96YAE)`!m znO>i*Zfw0r+rbISXXN+SCm6152J2O@-;%k=SW2A8=Ho-`%);`R(Jde?3i0J@;3IMe*23Ug4sjIO}hbbFFu%UsBn^KzU5%TMIq0R>?R~=$|qc zFHQs-JlxJ%v`h;>(5q_yql>Tj@*bG`s_W-;g;iE=WMEWqYwr^ zzh{@1>L@2`N=awhFN+y!na@)$E9P_IhU}XJsTztIMeS)v&I(9Pd}QP9Dt#wWbGAPd z-{2|bSN+kW`myUsjzS~O&&aa-$d&H_E}wKlW+{X|cq zXZo^xD(y{szc(enxqu+`39`((m`N`%L(x&0avr4we#bxjLGlWQDLS4<{XAT1{`} zvO(+2%BzLpz7fSpl$o4^@AQ1NI>^~uzNkIobkrX)m!WRGqWrDwv+7iZ z&Gygb&=-Lt>C=~=z4fPRko`?6r5K2v3@{N@5cAGDF>?xMh*P1aNS<()swTE;Dfh=2 zGGuYu7A|{!Flp3KXZbj(c!rnJMLI0~&KUg9ICWvf^OAASW@me0mpaX&ddb80JW~VM zR4q7%WD~J-*%^t4i~iNjiV}o_0WeFnmzA+sLa54#g~t=@sx9ZVIebg^MfN< zBO3CIVUgYHe#>i}Kd*re0QuTBih>-bX*)+^8a+Of@N3bPiH@CIl^bT7f%{BinRku* z9pL1K6~|PT>*-SxqAuu+{F%2bbi(KyXWt_S+sV_E=*>T>b^iPM!3>)bB67vO<_BB+ z8;3u4o%~KvJnLID57K=7)v6&~c~y6$jCbWu+T&KISHiD7bNwPBU*E`PGw9a{b*}vV z-RSF9xvq3=u$R{bIn!-HbFS;C@A?brv+_QTew#DZ87=Qxp-9P9#v`wr!>s+_WS%d&@g5xPxP9GIm#6dxi|Fz!9G$9pnFp z+zyo5k&%3LjN^J25xOF*N@;7R;lf9CV_-FPea4k3xS>Z}qe zi)TjyGDuioZ!^&meQ70O^oUSeXtuQh%~vn4k2-c=C}sX>6#U6${q$|Pt;o#>l8irw_XEB@FI?V;NX?6D&8S|8-AN>A zOD$QtXr6N3=v%{dv|EnW$L8ARf%s>#0urbxW8KRcPGyQGYYYP_QNO#g^(=c8YQMU< zT=!xy%U{${)huZL`piu{VrngBDf>;o*$>y#cNZ_&47hsu&8*FTSu^~`Rysx7o6$F} zG_TAw%Higr?c{p0CLf1bY5699_}b-ef{3PitvKSwn^|!ZtF>2lTdPGcQD+>!)5WUB zh3!Zw1P;&5-J-S%YQ@vg&aUN#-N7RU}qRxcE%!)WNyAE|r> z09kg&{ZYiv^cw1Lr`iPDAi+({X$A9$*p&w$#csINjJj1Y%|shTime^4 zC&``k3fT~gHimvCXxy>v9UaKjs@zKNRWRJXD{T{8|I^CTa1EF|3=4~IExNmmPL;H0 zF}K1D3r#EbaRxCzFqm&F{x$IRjV|n0U`nZeQ-dSf?y@}&ms4x*Id;f4Nwf0KGt2+h zt%k*u?5|WO_Pq9g`MM@gk-`|`%N0MWyk(CvUl5UVX?(7Fz)9mhbOxmwuQH)jRhwX6 zw;pqiDKqS*B4487QmWtyWE`2M+#h@Xvoz{}vOA77_JP|z=n)|b-r8V%pyvduYV>-+sKQS%N*TPsPA zEaT$vPxL^fth_)bO#II%C1DWY5 zh)5Wkh#|2T$?*}2s!FD-n@bG$DY&ozD66$a^^Dsto;qB8*v!2c7!=7r9vR?%DCsEg zG+Gm@KJR8|(XT7oNR57dV0XgYb~wCCrf>aCh)K0L(zinL@x9nd63#c}@|^my*mw<}zm{ zvqFaZ`&{}yzvp?@yVm=x^}c`o*6*xyk8}3kXYbFR&e`{K>D;=d4+y}YCS`Uqp*weO z3C0Gy=jv|l=z#|jVBY;9*@hiYs$n|#o&RX~op{VdK#va1W!%3tIl>cGLJ)W+<_ySA$>pT53|7!tu}ZRKHSi*UVzuyQr3l-*v*}{ho`<0|&}_9XkJ+akf_Wgs zZRr6Z0vIG5>prQpRJX&%G)Jw+%-@Wk1&0YIFa^seeluHUj~`W9lAmXTpEf0ibO$In zdk`I3jXn790hyC*9gfURwr)e_v3hifG$R5jWfrpIb(P4F*}5A11=$H`4Z%S(kJUQT zF^@IHT|^L-62#>1;aCp@^ehn2_85rge8TTUEd;>eHCKP29T0&n;-q1j0z`)Sh(A@mK%fLOCfh3n9qvMH@i)oe1$iMbu)e$g|!6_ZbiHzew z#!XAb5e0=5698MhNiz7tr=_B2)S|f^Ey>GkY|bTPXi_&gwH>S zzU3WvJYw@4vAO?gM>(|c0~BP-W;fQ8uIkQ3dTikq0QnS!=4y5qQ1uXR_K;KcywdEc z)tX??T6yEt-%fzs4mA|5fQr6cbG%$PtweX9cqgs4p15}3f6(>m0*>PaQiMItKFJ9K z7mKkvHVXz~%a{pQrZ|xV5rk&WAh12ytxjo5>}*B>-SfT=VFu56YYLK|dvr<9r#g2r zO{aQvA=^@#GE#c59$o4b*qoD@H_da>l7e0c#x$Qw`I*TIqI2xUk`{b5f%cmAVCV8{ z=%86x!gJor8iNyl$4ouC3T~!$eUP4lc7%O;-ofwL;I2&STn)EOIM0H3LfKR96bWE_06lqQkyrx8U;^04- zqv}T29Lw4o%cWVtrQIvgxhbx*CH{q01qv5^dh;*zUVRL9`j^c6B<%VO?FR1IeXX*) z{?^yD%6DStpZbv3KW7e)$db!y!B` zIXo{kd_FXqFD%J13|m-}<=j@fT>Ible`F4nFi=-PbE*7G<}`?~DnrRrA)vi=qNDs5 z=ydQ@Is4BJ0H7g;LKA=Hu zSFIe3)0^dLVr34`cx>cxnqE9qJ%ZhuoSjD`iBcvvwVzoZ$>d?+;mmFjs(xA~7mXwl zghO!)2boNOvKhwJPQax}2>B7;)cZi^7HHo(Spih9c*CYRYD@0V`hWC?OPf%<8{hwM zfduK$!JvM!ZrGG-pp|c+RrwzY`Lmhz-(*1=L4vwz|Hy(gf&~39$@<@T-v7_z|7i=y_yQZVp@OEC_i-Xo5&HSH01VAi5Vm@^mpf(|d_~|To zjQANWiFP30KZ-+uaD)(85}0Zu>ZATF#up- zqyPhG+fe^_rYsW%0HoqfvvlHXZe#t>YfKu0?MFSwXK*N+& z(yVvZf_Ih!wy@1jpndC#9-X$np<&{*L(+U$79TdZu#g`T)O~zK85){9VE=$?FUocQ zp*BC8KJT5y7oJ;CSb9p2P6aA_paXn}7-9;63YTh8co+mSKO1_&vUHi?4^g5y#<8%l z5V~&&3?PCbFFU3T{Fc%S?O@2ud??Y*lw=aZwp9o~j%a`ZL<3x=HNOJRqFucX=$I=w zKqwRokyaWaITSWDO5~YVT1sl{wrDgzGKXA=pF0t)sFstB7S#~UNkS8|C&pd!9>4Lu4-1gN8ka5vpto=xp#cPC}drROnpOSWJds zyg9ZV5G)=ogD-y6kvX0a2~=AvEwFk*^+%cqr4kIFPOKg7d`+0f;Mm;spkWg4u5{k; zTu1e|T<`Sxu-rnYxa?y{LE%5_iANmU$=&>qHM!ff9Ps8XJcTbR4Gf2Z@da*S;qu2S zyjh35tVSRsLRM;kOa_^-1%G`DoB}}SE&%-HR$f8zS+5$Pi;e()Uj~W5kx8bFa{8S>UIfKP~ARl0}arOX1*A|Jcj+R4n_cRFPO05TF~ht&Q!W6Vg}|4 z8UQDSJgOKfvcLt^bHP+LJ?d&Xm5djH7z1@RG)gLy<>py~&}L48pJ$}!5!wA_ks>@p zkR~}v))B9&8I<^um4nvBt&E9iC&4Pl+bd1^2^yLrpBZlpf1bk=51K`Y$h@V|j1>87 z7DWBIiNU70a)=|6x}vGqtfZ2}Ch}EH<*-v{4I^h{sbHni?M`BlEGGp}+{03h8oj1d z4tJqb0)`odzJx(RVuOzb$mQ??bfh%&V9*z*%mg(0IC`LJP*(|=lN~8CcZ|T-r;bRw z4rx3Bs#Ftt(aWhG(2nE6l{=>&(&q$$*3b}ySn)xu@O04lX9u(_$VvFK1KI|ygQ*}H z$8^~Vf7ig!QeChz0nK-UNQg6o*c`Kh21pQE;|~I@p$YNB%Q(g!j|P91$ee!swiB$` zZOFiA{u8=4{bsiz_9t|XC6FKuWa0^X=-3S)1Skx3B7=1vlQCNWCpi;O#&PV~0zmOH z0j(~m@|YusF*`|#o8Chg9nR-Li7qZs>O#zMC5DRxB?KCXk|v@%6v^4`w2&kviHtIz zlV2t~@n_mlKxeEX00A*%MC2I_C=$Sm;8KHuVB!!;Y7SH+0paxbUJ`(S$1-tHzhdO# zF(6cF@&x784KZf`6ElnB*|R-%m{YaY%P6=aT!|`zkPiVPP=g}?40c>#9j{@3-ba{{<)I6<5uP7`+(=d2=UROD)j zi@{0bL~trN-UHN=m7;s0k6RG%hu17}l^fI+7p^2zaDm;e*@L~s!^7OweL1J{8Net* zD1)E44rAFOr{xGubj03oDZk4XU)syxxK_%idvks#^!|vxf!B*X>6y&JJD=}x_b-w# zHroRbc6hCe;;U|kt)>0ip@oajjr;Vd+bl&~m&J_xgs(ddmPoNy10>-5G&29|Q(}84d!jz#Q|G*E#-Q$b7UATQ}W2WwT z9Io)G9$2YWehIt!f&|qbx#`wJ0Q`oXAH)e)TBclE;J6yEG#9%6R880nPA|c||IS1* zgoNyXJLY+(Uell@QwUik2Nh$Z-L%~V4jIB3R~M}?d658&Pn&Mzd^Rnnzdqa*mdx=> z^kaNfruDS_m)=u07aJ_6oE5o4GW*4==hFc(2yT4(a%6gz`2CNzg?@YCysI0rq6KC> zlEDQ2e$~+|qg5BS)QgcMzwZyT@ac>hI^;jZ6c|w3+fFJ95Hz z$Of5)gkq6#iM_a;&QnelJ{jrtKKed!Ce}A%O}e;xTgB>bso!6q9c&(y6T2fiTk;rb4u`0~kLM`UVqha_NE)3P|x$$-9* zDo?IDL)e$pI4DApQnBlthiW9y}vfk^7Li3@8kdxnEF|j6v&{lO0cct&P3EH z7X3}&+G(X!DhAt({=N71r#tFF0u5&8XPOu{HThUj?D{s6cV}`{9ygqe#1^NJPk+}X zu~$R%J#g2HEB;msOm4#B*kt{BC>?Kz z@IKvmaL2BzuNuJJPW{UIT^_tst^93!boFN26@i2Grq;)g_IO$*UUYOb0K|H!&WoPZ zo8&Jd4h>e?s@SRCxEL~1Q~Y>LdY6^-Xom|m)*-S^L>X(e()@I>^&?k5Q__#`Gm@a; zJZh(vXm~SUnzm6?%i17o{k8dx4v!KK!iU0@l5sQ5=Kx>CsnXnOz8S49SXn;_+p3c$ z{L0csw`)l@uMeg32)gfK`J;*lWv8NBKUL?T;7VLaEpHr{kpl#7jeNZsD4hr`=BnF$ z6CIVkCB&?z5kqS!g|Z&49d{NUlFwKvDjKH83%g}(_sc(7Jlio$I}ef_#5VdgDZPed za-V9B?zy|iY?{1&Gx+_BAMJtKrf=t}N4RvO!t-`Lf$cY}rM4zw4<78+3|Z$aHfyc* ze^e!Tb9sE&)&C05Zy`w|B=OxZGdZEEr!oEe-Trqe42tGH{*(?CWOf{XSFlJfHskmN z)*$2@^1?1ofIxeZ*QS3e4l6J6V9p8Pk?iO_> z-c`raP2F(eS8MvmK5FHcQ@=33m-Hv4pYAG{8vZyrM4~k);u#Iyj&@dKJl!tK z?>yyO-@lhLm&f~JsI0^9z28QDy|$nCT?BuevcZnETKUKiovrKOQvl|9GiP@Hv$rmp zxmp@htX%JBBRG5d33Am`G>5dZR!Q#1x@D?JM+fkXxn8VHW9>M)kt(xQV*>2%XmuFi zN_PUwvYh-bU8161bxOOGdY|P>n#}UJoQ$!6jMI}rWdXl%F3nm@Y4uXgN)Tra%8eB==$7iZnx|jsf9PJF^xP&Nx149KBW3P^-{Bw=0CVmYvz|NwP6*O( zKJSq1X9;n4;yvG&Q{vWPt??>)s_Uu`zTEugp3NXLpz|%+Ct{SmgwhtC}l*alBTv))_WA3xFm(NNI9I=9ESCzWS}V zwS`#K8;L(<)+sKi`i$lHXHA@utR0QuRUH?;+NK#kx?8wg&Tg^L;*e{m?qYmk-xM(} zZ591ml{i#tNy^FWYKY#Gu*~1n_6#0n9=%={?ISh3R&W~`Hg>C@ zX>R=>zYVUQ8T{H7yCRKwF89siXqImAH~$SOHMzY@FB8N?cSFA&{A#Z!^^$(g(2mL~ zUL$EJ<9okVF8i96JFEeoF>>`~_Ju@Al-telc;R9%u>4wieeg^YBJ36aFWDIRjOWTM z&NaD|;D)tgzll=({S}JG-T8OlcYAvqzu$lHje5JX2@#mPbX%O)uq5}gzMDc?qI;EZ zQSsggo!GKZr4FTFxq`i8I!;%;!j@3nKE-{}h;U77G&j%u;5BLgO1;ugD`VS4Sa$L7 zi$~)bK4NV3OShxWKK!1&=lYj$Mq9Jg6OqBRNjHt%{lkrE04F8 zlPvtJ5Bi3{NS&jMbQ#a-i}^w>Uth2A8Oi%0MWf0_mwBtJ_*Ku>E~d$SX+CP7^k!41 zw5amiG%M5B@QNyyd`)=erb>42^%t^VZ47QJ*8f}~Ua^FA36De;+p9jN0C&s5T7BBV zu88r6sAofxH5W^henoAjKK1vVn$?_dF7lLNNY~-0uL#+HinE$c6%mkg_i^7*cz+YB zv)7isPUUUgcwx36ppp7~CkDP^p)jS*lH$LMlf0jCnbhO*X4iafT3MN!3+IT!zG;NE z*rDyuWgJ}S>*BO3EAH9ZaqN1$WQF@DPNl;dvQAkFC;eYf+J1`MG|?_L$F?gjQQobd zaABRBeehkDWue6I`=Vn&`X!l4pKi%(I;9tnEVi4CpNHK#>e{LoZr~2gHD>eh-zsYU zdaK@NT9{HuF#WvSea^hbXRa5$qHvqLWFbTP=LM_I;v8;%-xflNvfAtWCjY9-Tq~Dd z9eX9#j!^vFIDB4cNDI7;ELvtVE@2kbzs&CZZt7h8q2G=_YoUQ)r>pm9j#Ycn;7DsU`!ByQZ)MDv9_7z&?GlQD@7iP6VYQA?F3-Gk0&J-k&$#ji^E@;7 z;!0EI>`;YJ5%lbIb-JSP*OJirzi^AVqm7N1%|-QoKU)tY4)&%Ub%eUs_6M4G&ey|0 zF6j>o0B?uJHoNL#Ist=6u=OuAZz39dFPOx=|m|6s4wA z_TucK%{)Ekar~OX^4AZuC?~jq%Ts>~WPLfOI~{SN=oD6JrC?M zlYbPk&f$wQY-SG&`RgPcFD&?wRpW&FbCc^xQlvznFob#_BgmvNEq^d|TqyylKClJ7x3> zb*Az6581VWR9u&*M`x`8)l#8X=@uW4#a@9GX0Jr4N$&mF3tpyhhW1A?`IT!h|{Jg}#bmIP{FIv9p zU~=m+2iu128MaawYC}&bnfHz+3!i+qoAOXcj9W}qpss}0=6fs?_^dSF-F&VAR$j2= zwpL_x6@ZYl@`?j3S~vVs*%6^#T~o7?V$@_AnSDOBrTa3^UniA4!&N<_AggxdKkOMO zK~0@i@XysudU*4*w)^~wk47S+u2Q$$nlG~OfSJ-(&_BOYV+Pr>CvBuhrcSSndy`v@ z+hIOuOxm>y@9qW~DO13)@91u30dh3EZhDhxiV>w+(_UW&r%H}ZeD5H0f z?E#8bqKTj-t^fhx88L)CAbo-EC`Rhnr;$sXhugg~F~~#1UPi?og9J5;oU1dFjqk48 zr2B@N49=e~t@{Bie?`)OJrlXqdo19)V@IJzm=$9ahj7E#8~Lw|{vXJEehbK7Co?w) z5SR^mL3Q~AiXEUPzAX@GLQn3_mNS6!8t7lGJv zd8DpZ6>#CFtv3(nw$N+=mO84v@}B61TAIA63U0CBdl%Qz=uXGQd-n%2B`>>LZpHGE z$&xc#l9UP!iJPMZ8DYzfI|4B^ zGhD=-#MRDsfvsOl!fHZ-8QQfo`V+qd#}h4Dp|}co?aSfV*&hmKxa>io)tiksB0{4I^m5rO^a8)IkF_KT)aqI~!L?dI}#2E7?ghu^55 z5AFw#_u@l%kYc+>n>U%IRK~H4;RnK+4Z=~s-KQ8NR*0`m2z@WQn8J}f@ z?9-GP3TYp@LgYI>0&*3m)x!uuQ`wLU?6lq68>*2>N}sOZJ&->2fk0WTBOwK{Z zEZjb5$Xh~j7c;YRj~#SO@_vgP8%sCVmZVr=YIL8{XskH9Hcytv*z(cTu*j&aVD=lO ziEba0gqG~JLW4fjYM+#g>oG5vdf#@o*QvSrzm}W1{HoO5 zsmOrJsYr1A4;DlyssH;T1OFS=_qBtoB)CgBAshi)i@=?@#}s6vBJKI~q$wpGy&hBF z0&mpEhG92ydnbDu8vlZUBcdva*NLRQ35+4JihLxX!vE%h`p~6Fn!<127f&@`7E6;` z4W{`~mwo->7ANTOd-TN#fQq~RD;9Nz1G8874M=6FV28a=#)^7ejLGPP{I&ICs!Jq1 zH|)BdpA*oHW?Og#_WxKAj<5TgLpz)C)8s|(bl&goDtCLa&z%BV9*k9SNbFu?T@lgF z4tyTPJ^ElF-gYY6%YDu%cS*F}>vBEA&W$sF)ys=9CKG3c|HU-kv=Z>;bhEg9eTuK! zj=Ri5TG}l)UC&1aa^69(^;?d@7S=53#o`B^>bF}ykTR%qWjGtZ&9;72FU{jm7*7Nk zF(2sEs79WB8+QGO?Pen-JK|{Kz8>>9iJK2am_(`2GTO(%*>0lr>ud_AQ=rmZFQw^V zc@+W|(IHX!apjC|fczQ@u!P;d9~Fzfm}F(kNKXg^1e>)R6$3bGAI!XeajkcnX=QMb zcB{K_WffpVeznH1pG9mZ{b1FNjU84L=O82GrL6ynvJ646@(9BAwgfs5<#XFKR~qZkvS|4(Nq> z9Lzx@Wbv??4f`^$Lu~nw+a@YG^U8s`Z#n7a_!AAxShGNvKxzHRmdjzQ^>7h*P(0<) zX1V_5V)VUzYrApO>lgm8@1H!SFShS|=D_g#a)f1>Y@AY$->PP((emc{(7o}++Vr$i z-o)Sq=~snW{4tLCm|9h!)yG9i32rva{mKI#k#BE#@gByf5k>iQ%>P6-)XZ!gNhGc| zhXu2+z$WBz{V)7IduBUEdpPZp!XAZ$j++xM>G;X@2sfJ7d* z1Y3%>8z>_VP7BN*83g9PVF9^+ZD`P&Q6q1lxyJs3 z=jWJO!dHVFDM>Hx)KH2&tQ-F_I-Zz_sZ`MK_(~tFD8R2d`Tpn8)vk0lkYDV3^wiL_ W$M7xRwiK)7nff53xQNvzNI~-c3hNelD3})GemKO)+uO+!hcyZcq|-vMhYw)`5BtRVVfR>=01O>B)d!4$9A5zB0VuEATzI1M?(&NhCoegpPoxQElXcDm z$J3hO?A_fOwG%xMM#mCDlmU=CbGeP>{?y?qUM!`$XxI-{YNjJ~Oq}Ufc^`Fzu zdq(=fepU-5OO=GW10qVBiKe({r~E$PI7LAjxay*yZd{E9{UN>sds@!+byc1xX&tW0 zlWfeb%BPUcM*QjT?n*tUFF5>yWy+HvlivrS9$5G+5Evo^3OI}SiPXXZhCp-ITy#%^ zM(?0T?+1;31${3?{Xq+}VTzTvrM<6@(_H^ED2_6ofrran55h&W70Z%$|YMroE4f_crcxPshItmTP7_e zfu5BRJPnvg$e2yYKA6Z7j_jIncJ+VSiD8|_fCz2baa2Rpon!Q*gcj}u(5FO-UPp|n zGo8>uw{wnl?1**i%J=Ew?Uni63DE7Z5tU4xyzM&j?E00g`iBXDbd!tVZ z;{__BeLl1>f`PKk2^5-z0okfnpth|SNrD7v6;oLD0DGy|MvgsQ&PA6Df0yo4V%W?r zDxvq;%$7#=VJ1uIeYo!8w({ZuHoebvJ)0w%B`TIkUEvNv7-qIqt{oEy>B5KDQrxlK za4&U$J#(oU4QJ{3C5F6a9}zzxQ+gkFUuoZWHm~50^rt0}&Fcc-Y$c@hUGF*p49Z;_ z)(OLh2>llLM790=DQr?UG6=0*t(~V8`;A;)IldEDrInSvoyL$VowP*#{$5V z|8o>;r>X{}Y1fp-NE=_OVr3Y28#O(&=rI)xb*+82bJtqsx2?y2J480Mdh8dR#hWRk z*W0*6a(E;DCL9>xIF2Zzpi{9SISxpSFh5d@VqjL3pOXHwsrP@-fWa(^ zXJIh=?uw75o{Jm5krMDE?&WCG%h9a;8&hf2!oPHp>QECPNGD-B<_BfsJ4#P`kk*MD zyp}o6N6{U*8eO@1I~w)ehfI4H2|JevgGx>?T)dS729@K6RcPK{1`J!#hJ9&|<7s2p zX?t!YQLiVxT>euZjr()vz_ih^$)0riYvyEsZ=J2{%F?5<41}4(EiQY*t^8Z&l!s;1 z6lK(;Wz42!8>JV9r?YFDE2FyW=WhMm{zvA(gn`|OqUZFN%;DoTw!!3SG<9DR>1aO) zPls5ijem9k0Q{WwI|N6Go)^XJF~#aJ#mv{o-uK_N$AZ*?hAjX913>GyQ$sQRS>-Oq z&b$$p*R)=nhFV7XDL=ZbG9k_NtagyJwgJ9S&bGSrk(xISL-(OaY5SzP%Gy>_a7Z&0 zj9V0_zyP@pgkF!B-VSl-M?rHP2E9w5XGydI#32HIkw9FsTU-CH{`B0%3H^e;#sv!6 ziAKOKTJsm}_)7MCC8z(DkhRSkf0G3@f`a;$)?`7AprHSftpCRI{y&fZ(-weXhmeCm z6;g$^i5SBg0nX?-u&}NJ1V#vTfDoqUri7)c5M-=jr|2gz;0uc-&0M0;xM>KlGvV*BamZ{7E zD(DptQzN1g0%9@2)2S2mRiumvA^JdG5=?ladxi*JQ`Uu?@XF3y(;dT(xwLThY_S2M zcganmayGGgUH=l)J3OPZRt#P*(Cm@|vws;zHkI=X>m&oL9^k*SE}TxRe4SVs#;)x? zYTC2p=!JIo_Vq283@e;XuQXy;*VY1DvAZ!j8jOF)GlC=x!5WS1t$p@n%3~Q8x z+M4uQBNo;l=jn1J1R#p6oe||>uIM8)w=&>Q2;5a>m{A?>%BoH*n@z8-jbK#?A>7(O z>?uG9?GzKRW=;K`$}oX>Yh?sQrH}7uilD%)t)07nU0~MHxWI8}L}(>GG#NBur{MZF zl>uPq5de&@1;#P+DjWFlq7xI`mrOhg2WzfQY?VC81(%ZZ*YHO$d>}wR@(L`dB4vZ& z0B4G`b%Lc3_Fo-R(pNTgZg_>{*gU&YGR3RDTcq2h|U8)6?2(#;| zk#E~B<&$^d+f^gY+h+vCBmQUcwl@@aWa8WXnF{S~D#3NUQJqYM#v}tOh+vI8Ie2enyB8)TiM%+Cv)w0#>T|LzKBvI@}>FT=hARpgtl5g-kYs ztc)Nl0UaFIcEBZ3jSC#*J4GfpAX1sigkqD8`@`r4Mtl$6zQOIiu z3V8?QCy*h;3P+!{C64n*uuX*ZkAw!M7>nq>edHer*^B7HuRuX$T!Dx^Ja$it10qAA zKqaUKvrFrwt~AD=To`*H0Ia{5PjNMKI;F!=ttuqxE7LtG8AfzDN*$Nfhn>+Y$iOr6 zPy1L%6;K{JNNdyFaL6kKsxgbD_WU8Ek z4lz?qeDeDtB*3C_F**tjs(J=K;!bU`;Mz@o&IW){QxD&`ae&5&)z>!FFSHtDK>+wDSDq12e8W1Qztq)66hM5T3=wbb#(Rg4Gh<=9VGYymWR^v z3JSd%&SyHLT+%vGuIXLt$N*sBaSa;^3lIolfhAlcMBgPO_=-X-5PX*q++Z^F`d<|7 z9DQ6O!n}8Rt-7oluNtmegw5NJ4$EJit}j|%Kg%7KwDaE6n})rx3CS|Gp{gI$eW^eD zp~+&`iTRnhyZa(r7Od4z1Q4?qkmCs-g5@V+2+cBEXt-{#3rPb*=46Z?^s!lKeMG~4D&;Nek^8##HsH#xF%2c z=6_h`bPR1r-*G6F3Jcty60QC!u;oaed}NtBFR-|Db$s#ai|$aZirhCbn<9r7HqT6d zp82U|u&(CwPu4M03ySte`z3GOx{M0?F%S`w+9UO7waQ*eV=MMI%VRJ45^fb`S{Lgg z8@R*zenHp@`nUx z12JZDuPdeT?4#AZ$e)pcB@f@&fwy)CzM01ar*>I6x*q5p(@MQ-c6IX}i{cMyoKxZCJu(&$6a_qz>|KZOoP5XP7hVi|@+3|5@BhBM2 z^PlOX>W+tou@5YwGqc7Ud>hL`GPm-JPU7`7Yp!GShVPQUCdHVY*BV#8Q8V6gU1rpV zrdc6wHF(`>Lrzt64}~18=dh1*LAzbBSY(YJgRNa6*Nf| z=RVk%bYV|P$2+@q;zI=0@85qMns-FR9(QQ}s5<`4Kzt?nZs_jW(k*y(eAL|2UBR&^ z?uF2}?x4i|V+oxL*ZMoD@r{)^ocBx7UoG!{*xVOK|5~NCzv24y%YFB4Qzpqfwr@V! zXCTX7c`kmB6SD8HUwOl#`rD(_;4LlNNY^4Kr3Ulcj{aO#eSNoY!B~8BG|!ERZd#l$ z$L;bZo3@&x+H^?YzPY_n8oS@)PW+B&q&lUuI*)s3zUa13oizEP<>f5pEM?2a6V20< z!LqJ*jt?tzwzgzz#4Z0gwy(3B{H4cEJIA@thBv(ZeW@~c9wa@!W7_95^PAbubEk9O zX#A?xw41v9eD674V@%i5vrUEH#MTZrQ13HFL)>W^DO~NtzD&()G@V?_^`1MPFHIcf zzCF5vvZ#!}`V*QQRfsZhXMM3e(Gwm+Aa&LY<_D0gEky-v^{;DZk>{Vz+$M-e4 zG$c7=)9alI4zEt)CN3v6Y$@H(%)c`^;qT02wov039Ur}a6PF_aw3+Ur?%yqkghGyv9Z7cEt!^a``c<4x1ChW+5JB2ixVAtl_Znh4}GbN z8EcQ~-5Y$ZxV6zONHtYK8w;=%?iwBg?W|IE6Q`@Fhxgj4HAmt|-b~bXa=o4p`Pk<> znAgYXFE?oxvz%o$zZa8Llt`GYPJV?TrcM%a5>qSQz0mXyDQq^sz%kLF+?jiSAg2!b z=_b$rV3CTsW$X)na??}Ir{LV=?q8CMOQ%@3L5BDho}5wzrv7a2GYx4zB1u07-9K7) z@gI@TpUo$8aaugZQ&(*d?X~hntL@t$NB3xm*j~ZCDwS-jTC@MQ2~z?b!)i|C=s)*+ zb3sjg-4+Q8pHmF)V;++8nJ3$>?rrUB$&Fbukz4;hM)gN_`h(SvmY37E1)6)}p7kqS zeQ^z_@vZpJhkSRa)zwp?nmq56r1d3vC#vW)h4$wl|u zA){OLl$CdwumkNacvhh%LBY{Dc2~RYJ?ZDoEl)>ux=ISq7Ehnqy7KkDS;+&WTTPCU z%JHKyW_KK69;8joMW`NkOdP9PxD(;F&@w>dvabaA zTDsEWHynLmX4Y6^vcmrAgm;!}tBl^TlJnu;ewM#Y*`U@IoSI-ee(}C?KkGt(6>**J zA(K#3zfYgeJ&M^)K=}isA+OeJ(&HbCQ=&+xFim%w$9-}UH!aH@kK68_+!Bf26IE}}Y7W!TSPZ1@KT)APlWQoY zx%Kkp4rkYq-1BW2Pk8Zf`%*ZV`Th~bO>LP`7sI}ee;y`}+;h*sWSrq^)Q9BP)=@{= z{m*$)-o5|(vzxJALz8FW<-4%kp5)Sf~4_T_HB!66Hk3_sanfRjb*V?j| zKbpI%c=U}vPtM%y>PoCAF~$`vfAfB}aQ;jq z@|{v!yI7|z0879Aa|3)g`#1t>?@zz{C#bsW!7x~@_<~4Zv>>)ec31Iz41jc5$ESbx z-OB(@cEr!5V9ljfo8xFiMgrqwN_(;aQgr1D$I^)Krx|C$RV-TzX*2Vgd!tRZHt|BU sbMdDWqJmPQnPCI5X}7ZuI=k=A8c*FFKBcJd)=f`9J--ahjimwjA8}|h7ytkO literal 0 HcmV?d00001 From 12e06eb52d9111ee07b48d7ea807c642fcc01588 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 26 Aug 2018 02:48:04 -0500 Subject: [PATCH 05/75] Added option for visiting soundpacks section in website, from the help menu #247 --- src/controller/mainController.py | 33 +++++++++++++++++++++++++++++++- src/wxUI/view.py | 1 + 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/controller/mainController.py b/src/controller/mainController.py index d3c028f8..351d20df 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -2,6 +2,7 @@ import platform system = platform.system() import application +import requests import youtube_utils if system == "Windows": from update import updater @@ -167,6 +168,7 @@ class Controller(object): widgetUtils.connect_event(self.view, widgetUtils.MENU, self.check_for_updates, self.view.check_for_updates) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.about, menuitem=self.view.about) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.visit_website, menuitem=self.view.visit_website) + widgetUtils.connect_event(self.view, widgetUtils.MENU, self.get_soundpacks, menuitem=self.view.get_soundpacks) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.manage_accounts, self.view.manage_accounts) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.update_profile, menuitem=self.view.updateProfile) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.user_details, menuitem=self.view.details) @@ -1393,8 +1395,37 @@ class Controller(object): def about(self, *args, **kwargs): self.view.about_dialog() + def get_soundpacks(self, *args, **kwargs): + # This should redirect users of other languages to the right version of the TWBlue website. + lang = languageHandler.curLang[:2] + url = application.url + final_url = "{0}/{1}/soundpacks".format(url, lang) + try: + response = requests.get(final_url) + except: + output.speak(_(u"An error happened while trying to connect to the server. Please try later.")) + return + # There is no twblue.es/en, so if English is the language used this should be False anyway. + if response.status_code == 200 and lang != "en": + webbrowser.open_new_tab(final_url) + else: + webbrowser.open_new_tab(application.url+"/soundpacks") + def visit_website(self, *args, **kwargs): - webbrowser.open(application.url) + # This should redirect users of other languages to the right version of the TWBlue website. + lang = languageHandler.curLang[:2] + url = application.url + final_url = "{0}/{1}".format(url, lang) + try: + response = requests.get(final_url) + except: + output.speak(_(u"An error happened while trying to connect to the server. Please try later.")) + return + # There is no twblue.es/en, so if English is the language used this should be False anyway. + if response.status_code == 200 and lang != "en": + webbrowser.open_new_tab(final_url) + else: + webbrowser.open_new_tab(application.url) def manage_accounts(self, *args, **kwargs): sm = sessionManager.sessionManagerController(started=True) diff --git a/src/wxUI/view.py b/src/wxUI/view.py index 64a2597b..33596dff 100644 --- a/src/wxUI/view.py +++ b/src/wxUI/view.py @@ -74,6 +74,7 @@ class mainFrame(wx.Frame): self.check_for_updates = help.Append(wx.ID_ANY, _(u"&Check for updates")) self.reportError = help.Append(wx.ID_ANY, _(u"&Report an error")) self.visit_website = help.Append(-1, _(u"{0}'s &website").format(application.name,)) + self.get_soundpacks = help.Append(-1, _(u"Get soundpacks for TWBlue")) self.about = help.Append(-1, _(u"About &{0}").format(application.name,)) # Add all to the menu Bar From ec860aa6fcca4406695abe5e176438119c7d4832 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 26 Aug 2018 02:59:12 -0500 Subject: [PATCH 06/75] Updated changelog --- doc/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index 4f58faf2..4ae2c7be 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -3,6 +3,10 @@ ## changes in this version * If the sent direct messages buffer is hidden, TWBlue should keep loading everything as expected. ([#246](https://github.com/manuelcortez/TWBlue/issues/246)) +* There is a new soundpack, called FreakyBlue (Thanks to [Andre Louis](https://twitter.com/FreakyFwoof)) as a new option in TWBlue. This pack can be the default in the next stable, so users can take a look and share their opinion in snapshot versions. ([#247](https://github.com/manuelcortez/TWBlue/issues/247)) +* There is a new option in the help menu that allows you to visit the soundpacks section in the TWBlue website. ([#247](https://github.com/manuelcortez/TWBlue/issues/247)) +* When reading location of a geotagged tweet, it will be translated for users of other languages. ([#251](https://github.com/manuelcortez/TWBlue/pull/251)) +* In the Windows 10 Keymap, the action to read location of a tweet has been remapped to Ctrl+Win+G. ([#177](https://github.com/manuelcortez/TWBlue/pull/177)) ## Changes in version 0.94 From 013a80a1f28fd3f0f44734c61969e37f8ec31102 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 26 Aug 2018 08:07:00 -0500 Subject: [PATCH 07/75] Show quoted tweets properly after being sent --- src/controller/buffersController.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controller/buffersController.py b/src/controller/buffersController.py index 390dd3e9..8e9db99e 100644 --- a/src/controller/buffersController.py +++ b/src/controller/buffersController.py @@ -644,6 +644,7 @@ class baseBufferController(bufferController): if retweet.image == None: item = self.session.api_call(call_name="update_status", _sound="retweet_send.ogg", status=text, in_reply_to_status_id=id, tweet_mode="extended") if item != None: + item = self.session.twitter.show_status(id=item["id"], include_ext_alt_text=True, tweet_mode="extended") pub.sendMessage("sent-tweet", data=item, user=self.session.db["user_name"]) else: call_threaded(self.session.api_call, call_name="update_status", _sound="retweet_send.ogg", status=text, media=retweet.image) From 7b925b590989c408cb1a1521e59c4c27e89e6f3c Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 26 Aug 2018 08:09:55 -0500 Subject: [PATCH 08/75] Fixed a race contidion in the main twitter session --- src/sessions/twitter/session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sessions/twitter/session.py b/src/sessions/twitter/session.py index db0bc0e9..c1fd62b5 100644 --- a/src/sessions/twitter/session.py +++ b/src/sessions/twitter/session.py @@ -197,6 +197,7 @@ class Session(base.baseSession): finished = True except TwythonError as e: output.speak(e.message) + val = None if e.error_code != 403 and e.error_code != 404: tries = tries+1 time.sleep(5) From c374cdefe93ca502dd498a072612fef9460b8a26 Mon Sep 17 00:00:00 2001 From: Jose Manuel Delicado Date: Fri, 31 Aug 2018 13:59:42 +0200 Subject: [PATCH 09/75] Updated windows dependencies to remove pywin32 --- windows-dependencies | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-dependencies b/windows-dependencies index 814ea75d..c80247f5 160000 --- a/windows-dependencies +++ b/windows-dependencies @@ -1 +1 @@ -Subproject commit 814ea75d5d94eba97c159f4cacef798cd00f3b0d +Subproject commit c80247f5e1e8aba6fbbad4d20cf7c7fced6f51f6 From 9fcc5d36e520310d9670938bfceec408c34bce52 Mon Sep 17 00:00:00 2001 From: Jose Manuel Delicado Date: Fri, 31 Aug 2018 14:00:16 +0200 Subject: [PATCH 10/75] Updated requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index db4e671b..65691da8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,5 @@ idna chardet urllib3 youtube-dl -python-vlc \ No newline at end of file +python-vlc +pywin32 \ No newline at end of file From 8619deb6ee3cf4ad66a8068c15b9b34ea7628556 Mon Sep 17 00:00:00 2001 From: Jose Manuel Delicado Date: Fri, 31 Aug 2018 14:02:18 +0200 Subject: [PATCH 11/75] Updated readme to remove pywin32 as an installable module --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 024463e3..0c5bd5d0 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ Although most dependencies can be found in the windows-dependencies directory, w * [Python,](http://python.org) version 2.7.15 If you want to build both x86 and x64 binaries, you can install python x86 to C:\python27 and python x64 to C:\python27x64, for example. -* [Python windows extensions (pywin32)](http://www.sourceforge.net/projects/pywin32/) for python 2.7, build 223 * [PyEnchant,](http://pythonhosted.org/pyenchant/) version 1.6.6. x64 version has been built by TWBlue developers, so you only will find it in windows-dependencies folder From 940ace664cb4788c9fcd030843028f0e7e819856 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 2 Sep 2018 08:59:22 -0500 Subject: [PATCH 12/75] Fixed error when adding tweet to likes from the menu bar --- src/controller/mainController.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 351d20df..369cd4a7 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -154,7 +154,7 @@ class Controller(object): widgetUtils.connect_event(self.view, widgetUtils.MENU, self.post_reply, self.view.reply) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.post_retweet, self.view.retweet) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.add_to_favourites, self.view.fav) - widgetUtils.connect_event(self.view, widgetUtils.MENU, self.remove_from_favourites, self.view.fav) + widgetUtils.connect_event(self.view, widgetUtils.MENU, self.remove_from_favourites, self.view.unfav) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.view_item, self.view.view) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.reverse_geocode, menuitem=self.view.view_coordinates) widgetUtils.connect_event(self.view, widgetUtils.MENU, self.delete, self.view.delete) From bd56eb953b7c2c7b67cfadc08e22841888cffa72 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 09:09:25 -0500 Subject: [PATCH 13/75] provide better control over cursored buffers when getting more items --- src/controller/buffersController.py | 10 ++++++++++ src/sessions/twitter/session.py | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/controller/buffersController.py b/src/controller/buffersController.py index 8e9db99e..e052d4df 100644 --- a/src/controller/buffersController.py +++ b/src/controller/buffersController.py @@ -351,6 +351,8 @@ class baseBufferController(bufferController): items = self.session.get_more_items(self.function, count=self.session.settings["general"]["max_tweets_per_call"], max_id=last_id, *self.args, **self.kwargs) except TwythonError as e: output.speak(e.message, True) + if items == None: + return for i in items: if utils.is_allowed(i, self.session.settings, self.name) == True and utils.find_item(i["id"], self.session.db[self.name]) == None: i = self.session.check_quoted_status(i) @@ -776,6 +778,8 @@ class directMessagesController(baseBufferController): except TwythonError as e: output.speak(e.message, True) return + if items == None: + return sent = [] for i in items: if i["message_create"]["sender_id"] == self.session.db["user_id"]: @@ -1056,6 +1060,8 @@ class peopleBufferController(baseBufferController): except TwythonError as e: output.speak(e.message, True) return + if items == None: + return for i in items: if self.session.settings["general"]["reverse_timelines"] == False: self.session.db[self.name]["items"].insert(0, i) @@ -1178,6 +1184,8 @@ class searchBufferController(baseBufferController): items = self.session.search(self.name, count=self.session.settings["general"]["max_tweets_per_call"], max_id=last_id, *self.args, **self.kwargs) except TwythonError as e: output.speak(e.message, True) + if items == None: + return for i in items: if utils.is_allowed(i, self.session.settings, self.name) == True and utils.find_item(i["id"], self.session.db[self.name]) == None: i = self.session.check_quoted_status(i) @@ -1240,6 +1248,8 @@ class searchPeopleBufferController(peopleBufferController): except TwythonError as e: output.speak(e.message, True) return + if items == None: + return for i in items: if self.session.settings["general"]["reverse_timelines"] == False: self.session.db[self.name]["items"].insert(0, i) diff --git a/src/sessions/twitter/session.py b/src/sessions/twitter/session.py index c1fd62b5..1a08198e 100644 --- a/src/sessions/twitter/session.py +++ b/src/sessions/twitter/session.py @@ -164,15 +164,24 @@ class Session(base.baseSession): users, dm bool: If any of these is set to True, the function will treat items as users or dm (they need different handling). name str: name of the database item to put new element in.""" results = [] + if kwargs.has_key("cursor") and kwargs["cursor"] == 0: + output.speak(_(u"There are no more items to retrieve in this buffer.")) + return data = getattr(self.twitter, update_function)(*args, **kwargs) if users == True: if type(data) == dict and data.has_key("next_cursor"): - self.db[name]["cursor"] = data["next_cursor"] + if data.has_key("next_cursor"): # There are more objects to retrieve. + self.db[name]["cursor"] = data["next_cursor"] + else: # Set cursor to 0, wich means no more items available. + self.db[name]["cursor"] = 0 for i in data["users"]: results.append(i) elif type(data) == list: results.extend(data[1:]) elif dm == True: - self.db[name]["cursor"] = data["next_cursor"] + if data.has_key("next_cursor"): # There are more objects to retrieve. + self.db[name]["cursor"] = data["next_cursor"] + else: # Set cursor to 0, wich means no more items available. + self.db[name]["cursor"] = 0 for i in data["events"]: results.append(i) else: results.extend(data[1:]) @@ -315,6 +324,8 @@ class Session(base.baseSession): # Recently, Twitter's new endpoints have cursor if there are more results. if tl.has_key("next_cursor"): self.db[name]["cursor"] = tl["next_cursor"] + else: + self.db[name]["cursor"] = 0 return num def check_connection(self): From cf735211c92e692106ff83d1004bf5bcf9d55914 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 10:52:54 -0500 Subject: [PATCH 14/75] Initial file --- appveyor.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..c038e461 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,45 @@ +environment: + + matrix: + + - PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7.x" # currently 2.7.9 + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" # currently 2.7.9 + PYTHON_ARCH: "64" + +install: + # If there is a newer build queued for the same PR, cancel this one. + # The AppVeyor 'rollout builds' option is supposed to serve the same + # purpose but it is problematic because it tends to cancel builds pushed + # directly to master instead of just PR builds (or the converse). + # credits: JuliaLang developers. + - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` + https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` + Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` + throw "There are newer queued builds for this pull request, failing early." } + - ECHO "Filesystem root:" + - ps: "ls \"C:/\"" + + # Check that we have the expected version and architecture for Python + - "python --version" + - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" + + # Upgrade to the latest version of pip to avoid it displaying warnings + # about it being out of date. + - "python -m pip install --upgrade pip" + + # Install the build dependencies of the project. If some dependencies contain + # compiled extensions and are not provided as pre-built wheel packages, + # pip will build them from source using the MSVC compiler matching the + # target Python version and architecture + - "%CMD_IN_ENV% pip install -r requirements.txt" + +build_script: + - "%CMD_IN_ENV% python src\\setup.py py2exe" + +artifacts: + # Archive the generated packages in the ci.appveyor.com build report. + - path: src\dist\* From 071bcf55ef4f788ac1ede824cc9db495cb6ce554 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 11:10:20 -0500 Subject: [PATCH 15/75] Try to clone include submodules --- appveyor.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index c038e461..63600c67 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,6 +10,13 @@ environment: PYTHON_VERSION: "2.7.x" # currently 2.7.9 PYTHON_ARCH: "64" +clone_script: +- cmd: >- + git clone -q --branch=%APPVEYOR_REPO_BRANCH% https://github.com/%APPVEYOR_REPO_NAME%.git %APPVEYOR_BUILD_FOLDER% + cd %APPVEYOR_BUILD_FOLDER% + git checkout -qf %APPVEYOR_REPO_COMMIT% + git submodule update --init --recursive + install: # If there is a newer build queued for the same PR, cancel this one. # The AppVeyor 'rollout builds' option is supposed to serve the same From 967cc8da718a51883e2835fa39b56d5ec8d6df69 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 11:18:10 -0500 Subject: [PATCH 16/75] Fix --- appveyor.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 63600c67..dea25d06 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,11 +11,11 @@ environment: PYTHON_ARCH: "64" clone_script: -- cmd: >- - git clone -q --branch=%APPVEYOR_REPO_BRANCH% https://github.com/%APPVEYOR_REPO_NAME%.git %APPVEYOR_BUILD_FOLDER% - cd %APPVEYOR_BUILD_FOLDER% - git checkout -qf %APPVEYOR_REPO_COMMIT% - git submodule update --init --recursive + - cmd: >- + git clone -q --branch=%APPVEYOR_REPO_BRANCH% https://github.com/%APPVEYOR_REPO_NAME%.git %APPVEYOR_BUILD_FOLDER% + && cd %APPVEYOR_BUILD_FOLDER% + && git checkout -qf %APPVEYOR_REPO_COMMIT% + && git submodule update --init --recursive install: # If there is a newer build queued for the same PR, cancel this one. From ccdcc7676e8544fe6a696cceaed62d27ffec7501 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 11:31:18 -0500 Subject: [PATCH 17/75] Adds py2exe and pyenchant to the party --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index dea25d06..3aa3927f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,6 +43,7 @@ install: # pip will build them from source using the MSVC compiler matching the # target Python version and architecture - "%CMD_IN_ENV% pip install -r requirements.txt" +- "%CMD_IN_ENV% pip install -r py2exe pyenchant" build_script: - "%CMD_IN_ENV% python src\\setup.py py2exe" From 4aef2595b2fb9fc9d56b49bb77948a8942b50143 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 11:32:25 -0500 Subject: [PATCH 18/75] Fixed syntax error --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 3aa3927f..6d402e95 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,7 @@ install: # pip will build them from source using the MSVC compiler matching the # target Python version and architecture - "%CMD_IN_ENV% pip install -r requirements.txt" -- "%CMD_IN_ENV% pip install -r py2exe pyenchant" + - "%CMD_IN_ENV% pip install -r py2exe pyenchant" build_script: - "%CMD_IN_ENV% python src\\setup.py py2exe" From 8459135002a67c1682cd75a80bef1003cea6b4bb Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 11:35:31 -0500 Subject: [PATCH 19/75] Fixed other problem --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 6d402e95..d17a1bca 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,7 @@ install: # pip will build them from source using the MSVC compiler matching the # target Python version and architecture - "%CMD_IN_ENV% pip install -r requirements.txt" - - "%CMD_IN_ENV% pip install -r py2exe pyenchant" + - "%CMD_IN_ENV% pip install py2exe pyenchant" build_script: - "%CMD_IN_ENV% python src\\setup.py py2exe" From d4a73fb3bbcf45b9b52d1371c98198c52332b4e5 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 11:41:36 -0500 Subject: [PATCH 20/75] Add direct url to Py2exe --- appveyor.yml | 2 +- requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d17a1bca..a62b7dbc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,7 @@ install: # pip will build them from source using the MSVC compiler matching the # target Python version and architecture - "%CMD_IN_ENV% pip install -r requirements.txt" - - "%CMD_IN_ENV% pip install py2exe pyenchant" + - "%CMD_IN_ENV% pip install pyenchant" build_script: - "%CMD_IN_ENV% python src\\setup.py py2exe" diff --git a/requirements.txt b/requirements.txt index 65691da8..501267f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,5 @@ chardet urllib3 youtube-dl python-vlc -pywin32 \ No newline at end of file +pywin32 +http://sourceforge.net/projects/py2exe/files/latest/download?source=files \ No newline at end of file From eaf13a44530f8acc7cc20ed901ce61c2e42eaa56 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 11:46:51 -0500 Subject: [PATCH 21/75] Changes in paths --- appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a62b7dbc..d28071d3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -46,8 +46,9 @@ install: - "%CMD_IN_ENV% pip install pyenchant" build_script: - - "%CMD_IN_ENV% python src\\setup.py py2exe" + - "cd src" + - "%CMD_IN_ENV% python setup.py py2exe" artifacts: # Archive the generated packages in the ci.appveyor.com build report. - - path: src\dist\* + - path: dist\* From db231e07f8221fd0b3d40efd293c4cc7e02c274a Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 12:31:02 -0500 Subject: [PATCH 22/75] Documentation should be generated in a folder called documentation --- doc/generator.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/doc/generator.py b/doc/generator.py index a19ea972..1bdc1888 100644 --- a/doc/generator.py +++ b/doc/generator.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import markdown import os +import shutil from codecs import open as _open import languageHandler languageHandler.setLanguage("en") @@ -38,14 +39,18 @@ def generate_document(language, document_type="documentation"): """ % (language, title, title) first_html_block = first_html_block+ markdown_file first_html_block = first_html_block + "\n\n" - if not os.path.exists(language): - os.mkdir(language) - mdfile = _open("%s/%s" % (language, filename), "w", encoding="utf-8") + if not os.path.exists(os.path.join("documentation", language)): + os.mkdir(os.path.join("documentation", language)) + mdfile = _open(os.path.join("documentation", language, filename), "w", encoding="utf-8") mdfile.write(first_html_block) mdfile.close() def create_documentation(): print("Creating documentation in the supported languages...\n") + if not os.path.exists("documentation"): + os.mkdir("documentation") + if os.path.exists(os.path.join("documentation", "license.txt")) == False: + shutil.copy(os.path.join("..", "license.txt"), os.path.join("documentation", "license.txt")) for i in languages: print("Creating documentation for: %s" % (i,)) generate_document(i) From c806f17484346b25b43ecd9f0b06507de55495fe Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 12:31:28 -0500 Subject: [PATCH 23/75] Calls doc generator just before packaging --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index d28071d3..17789fdc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,6 +47,7 @@ install: build_script: - "cd src" + - "%CMD_IN_ENV% python ..\doc\generator.py" - "%CMD_IN_ENV% python setup.py py2exe" artifacts: From 8a6f73fdc29d35daddef5f9bd8e2fc9f7fa7c1af Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 12:33:34 -0500 Subject: [PATCH 24/75] Fixed a typo --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 17789fdc..ec7a55cd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,7 +47,7 @@ install: build_script: - "cd src" - - "%CMD_IN_ENV% python ..\doc\generator.py" + - "%CMD_IN_ENV% python ..\\doc\\generator.py" - "%CMD_IN_ENV% python setup.py py2exe" artifacts: From fbbd3cf00e5890e7557b7bc0513d6949bcf40e09 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 12:39:42 -0500 Subject: [PATCH 25/75] Call documentation importer before attempt to build doc --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index ec7a55cd..a428d218 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -47,6 +47,7 @@ install: build_script: - "cd src" + - "%CMD_IN_ENV% python ..\\doc\\documentation_importer.py" - "%CMD_IN_ENV% python ..\\doc\\generator.py" - "%CMD_IN_ENV% python setup.py py2exe" From a3d1052193dff9f3737355941176aaf9a1634355 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 12:43:51 -0500 Subject: [PATCH 26/75] More path issues --- appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a428d218..3290a558 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -46,8 +46,9 @@ install: - "%CMD_IN_ENV% pip install pyenchant" build_script: - - "cd src" - - "%CMD_IN_ENV% python ..\\doc\\documentation_importer.py" + - "cd doc" + - "%CMD_IN_ENV% python documentation_importer.py" + - "cd ..\\src" - "%CMD_IN_ENV% python ..\\doc\\generator.py" - "%CMD_IN_ENV% python setup.py py2exe" From fe8a3b15651006f35559ff3891333eae42fe4d92 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 13:07:54 -0500 Subject: [PATCH 27/75] Try to create an artifact after packaged --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 3290a558..f4e17b54 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,6 +51,8 @@ build_script: - "cd ..\\src" - "%CMD_IN_ENV% python ..\\doc\\generator.py" - "%CMD_IN_ENV% python setup.py py2exe" + - "cd dist" + - cmd: 7z a twblue-snapshot.zip * artifacts: # Archive the generated packages in the ci.appveyor.com build report. From 9a94b9201855030b62a0791c457ed4c1e8362ab3 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 13:13:20 -0500 Subject: [PATCH 28/75] Specify artifact name to appveyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index f4e17b54..f661de4e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -56,4 +56,4 @@ build_script: artifacts: # Archive the generated packages in the ci.appveyor.com build report. - - path: dist\* + - path: twblue-snapshot.zip From b16a6c5ddb1cd6335f965d6ed8dd82cca353d906 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 13:27:48 -0500 Subject: [PATCH 29/75] Add absolute path to artifacts --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index f661de4e..ed20d2b7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -56,4 +56,4 @@ build_script: artifacts: # Archive the generated packages in the ci.appveyor.com build report. - - path: twblue-snapshot.zip + - path: src\dist\twblue-snapshot.zip From ec61f4b4311d2cec8f2d0cc7c7a0160d7ed4a873 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 13:56:17 -0500 Subject: [PATCH 30/75] Cleaning up --- appveyor.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ed20d2b7..42740f7c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,15 +1,24 @@ +pull_requests: + do_not_increment_build_number: true + +#skip_non_tags: true + environment: matrix: + # List of python versions we want to work with. - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.x" # currently 2.7.9 PYTHON_ARCH: "32" + # perhaps we may enable this one in future? - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.x" # currently 2.7.9 - PYTHON_ARCH: "64" +# PYTHON_VERSION: "2.7.x" # currently 2.7.9 +# PYTHON_ARCH: "64" + +# This is important so we will retrieve everything in submodules as opposed to default method. clone_script: - cmd: >- git clone -q --branch=%APPVEYOR_REPO_BRANCH% https://github.com/%APPVEYOR_REPO_NAME%.git %APPVEYOR_BUILD_FOLDER% @@ -55,5 +64,4 @@ build_script: - cmd: 7z a twblue-snapshot.zip * artifacts: - # Archive the generated packages in the ci.appveyor.com build report. - path: src\dist\twblue-snapshot.zip From 099bc49761b4233d3dbe66ede43498b16b9cfdcc Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 16:50:10 -0500 Subject: [PATCH 31/75] Test ftp deployment --- appveyor.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 42740f7c..ab065011 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,7 +13,7 @@ environment: PYTHON_ARCH: "32" # perhaps we may enable this one in future? - - PYTHON: "C:\\Python27-x64" +# - PYTHON: "C:\\Python27-x64" # PYTHON_VERSION: "2.7.x" # currently 2.7.9 # PYTHON_ARCH: "64" @@ -65,3 +65,12 @@ build_script: artifacts: - path: src\dist\twblue-snapshot.zip + +deploy: +- provider: FTP + host: twblue.es + protocol: ftp + username: twblue + password: + secure: ml/xB8YEoZ7DmjzDr+KSNw== + folder: web/updates \ No newline at end of file From 3be69cb6a261a2c0d5372efd8fd74366e2243ffe Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 17:11:06 -0500 Subject: [PATCH 32/75] Tried moving artifacts --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ab065011..6fae1805 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,10 +61,10 @@ build_script: - "%CMD_IN_ENV% python ..\\doc\\generator.py" - "%CMD_IN_ENV% python setup.py py2exe" - "cd dist" - - cmd: 7z a twblue-snapshot.zip * + - cmd: 7z a ..\..\twblue-snapshot.zip * artifacts: - - path: src\dist\twblue-snapshot.zip + - path: twblue-snapshot.zip deploy: - provider: FTP @@ -73,4 +73,4 @@ deploy: username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: web/updates \ No newline at end of file + folder: web \ No newline at end of file From ecd2984a617cce71e442b67b7940338ab239fd75 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 17:14:47 -0500 Subject: [PATCH 33/75] Last test before real deployment --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 6fae1805..5d48a20a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -73,4 +73,4 @@ deploy: username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: web \ No newline at end of file + folder: web/updates \ No newline at end of file From 00f96bb7af5c6e77d9c0a48f63b0ab72daebe2aa Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 17:23:44 -0500 Subject: [PATCH 34/75] Real deployment with fake path to ftp --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 5d48a20a..24b4af54 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,10 +61,10 @@ build_script: - "%CMD_IN_ENV% python ..\\doc\\generator.py" - "%CMD_IN_ENV% python setup.py py2exe" - "cd dist" - - cmd: 7z a ..\..\twblue-snapshot.zip * + - cmd: 7z a ..\..\snapshot.zip * artifacts: - - path: twblue-snapshot.zip + - path: snapshot.zip deploy: - provider: FTP @@ -73,4 +73,4 @@ deploy: username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: web/updates \ No newline at end of file + folder: updates \ No newline at end of file From a9378988cbe183e93edcd7df7c34b6ec000757db Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 17:28:33 -0500 Subject: [PATCH 35/75] Second test with faked path --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 24b4af54..a3f31191 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -73,4 +73,4 @@ deploy: username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: updates \ No newline at end of file + folder: 2017 \ No newline at end of file From 3ae581a19f35e6112448a1ea82d10b4925627941 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 17:39:09 -0500 Subject: [PATCH 36/75] Let's try with beta ftp lib --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a3f31191..80fe2031 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -70,7 +70,8 @@ deploy: - provider: FTP host: twblue.es protocol: ftp + beta: true username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: 2017 \ No newline at end of file + folder: '//pubs' \ No newline at end of file From 6c29a4a18fb25a44be4eeaf14611fe8c87372931 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 17:48:18 -0500 Subject: [PATCH 37/75] Use absolute path to ftp destination --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 80fe2031..c9f9bcb6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -74,4 +74,4 @@ deploy: username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: '//pubs' \ No newline at end of file + folder: '//var/web/twblue/pubs' \ No newline at end of file From 8bac4b8ec6187dd3af7d6cc78d422e3f583f1681 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 21:29:42 -0500 Subject: [PATCH 38/75] Commented and fixed everything. Ready for pr --- appveyor.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c9f9bcb6..400a5177 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,9 @@ pull_requests: + # Avoid building after pull requests. Shall we disable this option? do_not_increment_build_number: true -#skip_non_tags: true +# Only build whenever we add tags to the repo. +skip_non_tags: true environment: @@ -17,7 +19,6 @@ environment: # PYTHON_VERSION: "2.7.x" # currently 2.7.9 # PYTHON_ARCH: "64" - # This is important so we will retrieve everything in submodules as opposed to default method. clone_script: - cmd: >- @@ -55,12 +56,17 @@ install: - "%CMD_IN_ENV% pip install pyenchant" build_script: + # Build documentation at first, so setup.py won't fail when copying everything. - "cd doc" + # Import documentation before building, so strings.py will be created. - "%CMD_IN_ENV% python documentation_importer.py" + # build doc from src folder so it will generate result files right there. - "cd ..\\src" - "%CMD_IN_ENV% python ..\\doc\\generator.py" + # Build distributable files. - "%CMD_IN_ENV% python setup.py py2exe" - "cd dist" + # Zip it all. - cmd: 7z a ..\..\snapshot.zip * artifacts: From 72fd93eaf698b9e0a023f573bf683954d5c3acdf Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 23:26:15 -0500 Subject: [PATCH 39/75] Added appveyor badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0c5bd5d0..418932a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ TWBlue - ====== +[![Build status](https://ci.appveyor.com/api/projects/status/fml5fu7h1fj8vf6l?svg=true)](https://ci.appveyor.com/project/manuelcortez/twblue) + TW Blue is an app designed to use Twitter simply and efficiently while using minimal system resources. With this app you’ll have access to twitter features such as: From cbc2e978c97672c89f743aedc94ef92f086d20cb Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 23:29:01 -0500 Subject: [PATCH 40/75] Fixed remote ftp path again --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 400a5177..d86aa412 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ pull_requests: do_not_increment_build_number: true # Only build whenever we add tags to the repo. -skip_non_tags: true +#skip_non_tags: true environment: @@ -80,4 +80,4 @@ deploy: username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: '//var/web/twblue/pubs' \ No newline at end of file + folder: '//pubs' \ No newline at end of file From 682f1e8bd40d5b814d9c4d773f8ed3d583567f08 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 18 Sep 2018 23:55:00 -0500 Subject: [PATCH 41/75] Skipped folder option --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index d86aa412..681f3983 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -80,4 +80,4 @@ deploy: username: twblue password: secure: ml/xB8YEoZ7DmjzDr+KSNw== - folder: '//pubs' \ No newline at end of file +# folder: '//pubs' \ No newline at end of file From f87312fc53df84ffd2875d88ed51a075881f1231 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 19 Sep 2018 00:09:52 -0500 Subject: [PATCH 42/75] Disable building in all commits --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 681f3983..929f00fc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ pull_requests: do_not_increment_build_number: true # Only build whenever we add tags to the repo. -#skip_non_tags: true +skip_non_tags: true environment: From 7a2ad3797d67bdf90495b4d12dc644ed1241db6f Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 19 Sep 2018 11:12:51 -0500 Subject: [PATCH 43/75] Added py2exe_py2 from PyPi --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 501267f7..a4eb000e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,4 +22,4 @@ urllib3 youtube-dl python-vlc pywin32 -http://sourceforge.net/projects/py2exe/files/latest/download?source=files \ No newline at end of file +py2exe_py2 \ No newline at end of file From 79512af350babe9640fe8cae5f2f849c2dd4528c Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 19 Sep 2018 11:47:13 -0500 Subject: [PATCH 44/75] Try to add a missing module by hand in setup --- src/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup.py b/src/setup.py index 43c41dee..ab758a03 100644 --- a/src/setup.py +++ b/src/setup.py @@ -112,7 +112,7 @@ data_files = get_data(), options = { 'py2exe': { 'optimize':2, - 'packages': ["pubsub", "pubsub.core", "pubsub.core.kwargs", "dbhash"], + 'packages': ["pubsub", "pubsub.core", "pubsub.core.kwargs", "dbhash", "oauthlib.oauth1.rfc5849.endpoints.resource"], 'dll_excludes': ["MPR.dll", "api-ms-win-core-apiquery-l1-1-0.dll", "api-ms-win-core-console-l1-1-0.dll", "api-ms-win-core-delayload-l1-1-1.dll", "api-ms-win-core-errorhandling-l1-1-1.dll", "api-ms-win-core-file-l1-2-0.dll", "api-ms-win-core-handle-l1-1-0.dll", "api-ms-win-core-heap-obsolete-l1-1-0.dll", "api-ms-win-core-libraryloader-l1-1-1.dll", "api-ms-win-core-localization-l1-2-0.dll", "api-ms-win-core-processenvironment-l1-2-0.dll", "api-ms-win-core-processthreads-l1-1-1.dll", "api-ms-win-core-profile-l1-1-0.dll", "api-ms-win-core-registry-l1-1-0.dll", "api-ms-win-core-synch-l1-2-0.dll", "api-ms-win-core-sysinfo-l1-2-0.dll", "api-ms-win-security-base-l1-2-0.dll", "api-ms-win-core-heap-l1-2-0.dll", "api-ms-win-core-interlocked-l1-2-0.dll", "api-ms-win-core-localization-obsolete-l1-1-0.dll", "api-ms-win-core-string-l1-1-0.dll", "api-ms-win-core-string-obsolete-l1-1-0.dll", "WLDAP32.dll", "MSVCP90.dll", "CRYPT32.dll", "mfc90.dll"], 'compressed': True }, From 3d310c0ee4ca6d23aa402291be67dd902d595a31 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 19 Sep 2018 12:00:29 -0500 Subject: [PATCH 45/75] ADded other missing module from oauthlib (really?) --- src/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup.py b/src/setup.py index ab758a03..2e5a522b 100644 --- a/src/setup.py +++ b/src/setup.py @@ -112,7 +112,7 @@ data_files = get_data(), options = { 'py2exe': { 'optimize':2, - 'packages': ["pubsub", "pubsub.core", "pubsub.core.kwargs", "dbhash", "oauthlib.oauth1.rfc5849.endpoints.resource"], + 'packages': ["pubsub", "pubsub.core", "pubsub.core.kwargs", "dbhash", "oauthlib.oauth1.rfc5849.endpoints.resource", "oauthlib.oauth2.rfc6749.endpoints.resource"], 'dll_excludes': ["MPR.dll", "api-ms-win-core-apiquery-l1-1-0.dll", "api-ms-win-core-console-l1-1-0.dll", "api-ms-win-core-delayload-l1-1-1.dll", "api-ms-win-core-errorhandling-l1-1-1.dll", "api-ms-win-core-file-l1-2-0.dll", "api-ms-win-core-handle-l1-1-0.dll", "api-ms-win-core-heap-obsolete-l1-1-0.dll", "api-ms-win-core-libraryloader-l1-1-1.dll", "api-ms-win-core-localization-l1-2-0.dll", "api-ms-win-core-processenvironment-l1-2-0.dll", "api-ms-win-core-processthreads-l1-1-1.dll", "api-ms-win-core-profile-l1-1-0.dll", "api-ms-win-core-registry-l1-1-0.dll", "api-ms-win-core-synch-l1-2-0.dll", "api-ms-win-core-sysinfo-l1-2-0.dll", "api-ms-win-security-base-l1-2-0.dll", "api-ms-win-core-heap-l1-2-0.dll", "api-ms-win-core-interlocked-l1-2-0.dll", "api-ms-win-core-localization-obsolete-l1-1-0.dll", "api-ms-win-core-string-l1-1-0.dll", "api-ms-win-core-string-obsolete-l1-1-0.dll", "WLDAP32.dll", "MSVCP90.dll", "CRYPT32.dll", "mfc90.dll"], 'compressed': True }, From 1ccb898f788cac6a81c4bda7b8a8dd165b794519 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 19 Sep 2018 12:57:14 -0500 Subject: [PATCH 46/75] Updated changelog --- doc/changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index 4ae2c7be..3e845ec4 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -7,6 +7,11 @@ * There is a new option in the help menu that allows you to visit the soundpacks section in the TWBlue website. ([#247](https://github.com/manuelcortez/TWBlue/issues/247)) * When reading location of a geotagged tweet, it will be translated for users of other languages. ([#251](https://github.com/manuelcortez/TWBlue/pull/251)) * In the Windows 10 Keymap, the action to read location of a tweet has been remapped to Ctrl+Win+G. ([#177](https://github.com/manuelcortez/TWBlue/pull/177)) +* When there are no more items to retrieve in direct messages and people buffers, a message will announce it. +* Fixed an issue reported by some users that was making them unable to load more items in their direct messages. +* It is possible to add a tweet to the likes buffer from the menu bar again. +* Tweets, replies and retweets will be added to sent tweets right after being posted in the Twitter. +* Extended Tweets should be displayed properly in list buffers. ## Changes in version 0.94 From 5c75be20d30ba2738b2c91f1453105d2b298a078 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 19 Sep 2018 15:41:05 -0500 Subject: [PATCH 47/75] Updated snapshot information --- src/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/application.py b/src/application.py index df9d00b0..04ad4aa1 100644 --- a/src/application.py +++ b/src/application.py @@ -2,13 +2,13 @@ import datetime name = 'TWBlue' -snapshot = False +snapshot = True if snapshot == False: version = "0.94" update_url = 'https://twblue.es/updates/stable.php' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/stable.json' else: - version = "8" + version = "9" update_url = 'https://twblue.es/updates/snapshot.php' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/snapshots.json' authors = [u"Manuel Cortéz", u"José Manuel Delicado"] From 9fd9d2a120fad84fb9b5af57c9cc49441f8662d1 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 19 Sep 2018 16:14:43 -0500 Subject: [PATCH 48/75] Updated snapshot info in repo --- updates/snapshots.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/updates/snapshots.json b/updates/snapshots.json index c6bd052e..1cb3114a 100644 --- a/updates/snapshots.json +++ b/updates/snapshots.json @@ -1,5 +1,5 @@ -{"current_version": "1", +{"current_version": "9", "description": "Snapshot version.", "date": "unknown", "downloads": -{"Windows32": "https://twblue.es/pubs/snapshot.zip"}} \ No newline at end of file +{"Windows32": "https://twblue.es/snapshot.zip"}} \ No newline at end of file From c716f4aa964ea21b72dbe4289bc27420090f6960 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 22 Sep 2018 10:18:25 -0500 Subject: [PATCH 49/75] When quoting a retweet, quote will be in original tweet --- src/controller/buffersController.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controller/buffersController.py b/src/controller/buffersController.py index e052d4df..57d4e84a 100644 --- a/src/controller/buffersController.py +++ b/src/controller/buffersController.py @@ -633,6 +633,9 @@ class baseBufferController(bufferController): self._retweet_with_comment(tweet, id) def _retweet_with_comment(self, tweet, id, comment=''): + # If quoting a retweet, let's quote the original tweet instead the retweet. + if tweet.has_key("retweeted_status"): + tweet = tweet["retweeted_status"] if tweet.has_key("full_text"): comments = tweet["full_text"] else: From 211d43aa30f10444e6dbce0a5e3b21b796bdaa31 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 22 Sep 2018 10:21:50 -0500 Subject: [PATCH 50/75] Updated changelog --- doc/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.md b/doc/changelog.md index 3e845ec4..41a0682f 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* When quoting a retweet, the quote will be made to the original tweet instead of the retweet. * If the sent direct messages buffer is hidden, TWBlue should keep loading everything as expected. ([#246](https://github.com/manuelcortez/TWBlue/issues/246)) * There is a new soundpack, called FreakyBlue (Thanks to [Andre Louis](https://twitter.com/FreakyFwoof)) as a new option in TWBlue. This pack can be the default in the next stable, so users can take a look and share their opinion in snapshot versions. ([#247](https://github.com/manuelcortez/TWBlue/issues/247)) * There is a new option in the help menu that allows you to visit the soundpacks section in the TWBlue website. ([#247](https://github.com/manuelcortez/TWBlue/issues/247)) From 01a6c65c822b4b21d288d13c9ac79ad1d73d5fbf Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 22 Sep 2018 12:03:59 -0500 Subject: [PATCH 51/75] Updated to Twython 3.7.0 --- src/twython/__init__.py | 2 +- src/twython/api.py | 109 ++++++----- src/twython/compat.py | 4 +- src/twython/endpoints.py | 340 +++++++++++++++++++++------------ src/twython/streaming/api.py | 6 +- src/twython/streaming/types.py | 15 +- 6 files changed, 292 insertions(+), 184 deletions(-) diff --git a/src/twython/__init__.py b/src/twython/__init__.py index 84e9ee63..dc161d13 100644 --- a/src/twython/__init__.py +++ b/src/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.6.0' +__version__ = '3.7.0' from .api import Twython from .streaming import TwythonStreamer diff --git a/src/twython/api.py b/src/twython/api.py index 41f7665c..10c4963c 100644 --- a/src/twython/api.py +++ b/src/twython/api.py @@ -19,6 +19,7 @@ from requests_oauthlib import OAuth1, OAuth2 from . import __version__ from .advisory import TwythonDeprecationWarning from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2 +from .compat import urlsplit from .endpoints import EndpointsMixin from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError from .helpers import _transparent_params @@ -134,13 +135,13 @@ class Twython(EndpointsMixin, object): def __repr__(self): return '' % (self.app_key) - def _request(self, url, method='GET', params=None, api_call=None, encode_json=False): + def _request(self, url, method='GET', params=None, api_call=None, json_encoded=False): """Internal request method""" method = method.lower() params = params or {} func = getattr(self.client, method) - if type(params) is dict and encode_json == False: + if isinstance(params, dict) and json_encoded == False: params, files = _transparent_params(params) else: params = params @@ -155,15 +156,14 @@ class Twython(EndpointsMixin, object): if method == 'get': requests_args['params'] = params else: - - if encode_json == False: - requests_args.update({ - 'files': files, - 'data': params, - }) + # Check for json_encoded so we will sent params as "data" or "json" + if json_encoded: + data_key = "json" else: - requests_args.update({ - 'json': params, + data_key = "data" + requests_args.update({ + data_key: params, + 'files': files, }) try: response = func(url, **requests_args) @@ -202,14 +202,14 @@ class Twython(EndpointsMixin, object): error_message, error_code=response.status_code, retry_after=response.headers.get('X-Rate-Limit-Reset')) - content="" + content = '' try: if response.status_code == 204: content = response.content else: content = response.json() except ValueError: - if response.content!="": + if response.content != '': raise TwythonError('Response was not valid JSON. \ Unable to decode.') @@ -235,7 +235,7 @@ class Twython(EndpointsMixin, object): return error_message - def request(self, endpoint, method='GET', params=None, version='1.1', encode_json=False): + def request(self, endpoint, method='GET', params=None, version='1.1', json_encoded=False): """Return dict of response received from Twitter's API :param endpoint: (required) Full url or Twitter API endpoint @@ -251,6 +251,9 @@ class Twython(EndpointsMixin, object): :param version: (optional) Twitter API version to access (default 1.1) :type version: string + :param json_encoded: (optional) Flag to indicate if this method should send data encoded as json + (default False) + :type json_encoded: bool :rtype: dict """ @@ -266,7 +269,7 @@ class Twython(EndpointsMixin, object): url = '%s/%s.json' % (self.api_url % version, endpoint) content = self._request(url, method=method, params=params, - api_call=url, encode_json=encode_json) + api_call=url, json_encoded=json_encoded) return content @@ -274,9 +277,9 @@ class Twython(EndpointsMixin, object): """Shortcut for GET requests via :class:`request`""" return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None, version='1.1', encode_json=False): + def post(self, endpoint, params=None, version='1.1', json_encoded=False): """Shortcut for POST requests via :class:`request`""" - return self.request(endpoint, 'POST', params=params, version=version, encode_json=encode_json) + return self.request(endpoint, 'POST', params=params, version=version, json_encoded=json_encoded) def get_lastfunction_header(self, header, default_return_value=None): """Returns a specific header from the last API call @@ -513,19 +516,27 @@ class Twython(EndpointsMixin, object): try: if function.iter_mode == 'id': - if 'max_id' not in params: - # Add 1 to the id because since_id and - # max_id are inclusive - if hasattr(function, 'iter_metadata'): - since_id = content[function.iter_metadata].get('since_id_str') + # Set max_id in params to one less than lowest tweet id + if hasattr(function, 'iter_metadata'): + # Get supplied next max_id + metadata = content.get(function.iter_metadata) + if 'next_results' in metadata: + next_results = urlsplit(metadata['next_results']) + params = dict(parse_qsl(next_results.query)) else: - since_id = content[0]['id_str'] - params['since_id'] = (int(since_id) - 1) + # No more results + raise StopIteration + else: + # Twitter gives tweets in reverse chronological order: + params['max_id'] = str(int(content[-1]['id_str']) - 1) elif function.iter_mode == 'cursor': params['cursor'] = content['next_cursor_str'] except (TypeError, ValueError): # pragma: no cover raise TwythonError('Unable to generate next page of search \ results, `page` is not a number.') + except (KeyError, AttributeError): #pragma no cover + raise TwythonError('Unable to generate next page of search \ + results, content has unexpected structure.') @staticmethod def unicode2utf8(text): @@ -587,6 +598,8 @@ class Twython(EndpointsMixin, object): if display_text_start <= temp['start'] <= display_text_end: temp['replacement'] = mention_html + temp['start'] -= display_text_start + temp['end'] -= display_text_start entities.append(temp) else: # Make the '@username' at the start, before @@ -598,8 +611,8 @@ class Twython(EndpointsMixin, object): if 'hashtags' in tweet['entities']: for entity in tweet['entities']['hashtags']: temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + temp['start'] = entity['indices'][0] - display_text_start + temp['end'] = entity['indices'][1] - display_text_start url_html = '#%(hashtag)s' % {'hashtag': entity['text']} @@ -610,8 +623,8 @@ class Twython(EndpointsMixin, object): if 'symbols' in tweet['entities']: for entity in tweet['entities']['symbols']: temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + temp['start'] = entity['indices'][0] - display_text_start + temp['end'] = entity['indices'][1] - display_text_start url_html = '$%(symbol)s' % {'symbol': entity['text']} @@ -622,8 +635,8 @@ class Twython(EndpointsMixin, object): if 'urls' in tweet['entities']: for entity in tweet['entities']['urls']: temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + temp['start'] = entity['indices'][0] - display_text_start + temp['end'] = entity['indices'][1] - display_text_start if use_display_url and entity.get('display_url') and not use_expanded_url: shown_url = entity['display_url'] @@ -640,26 +653,30 @@ class Twython(EndpointsMixin, object): else: suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) - if 'media' in tweet['entities']: - for entity in tweet['entities']['media']: - temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + if 'media' in tweet['entities'] and len(tweet['entities']['media']) > 0: + # We just link to the overall URL for the tweet's media, + # rather than to each individual item. + # So, we get the URL from the first media item: + entity = tweet['entities']['media'][0] - if use_display_url and entity.get('display_url') and not use_expanded_url: - shown_url = entity['display_url'] - elif use_expanded_url and entity.get('expanded_url'): - shown_url = entity['expanded_url'] - else: - shown_url = entity['url'] + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - url_html = '%s' % (entity['url'], shown_url) + if use_display_url and entity.get('display_url') and not use_expanded_url: + shown_url = entity['display_url'] + elif use_expanded_url and entity.get('expanded_url'): + shown_url = entity['expanded_url'] + else: + shown_url = entity['url'] - if display_text_start <= temp['start'] <= display_text_end: - temp['replacement'] = url_html - entities.append(temp) - else: - suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) + url_html = '%s' % (entity['url'], shown_url) + + if display_text_start <= temp['start'] <= display_text_end: + temp['replacement'] = url_html + entities.append(temp) + else: + suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) # Now do all the replacements, starting from the end, so that the # start/end indices still work: diff --git a/src/twython/compat.py b/src/twython/compat.py index c36b3269..7c049b00 100644 --- a/src/twython/compat.py +++ b/src/twython/compat.py @@ -25,7 +25,7 @@ except ImportError: if is_py2: from urllib import urlencode, quote_plus - from urlparse import parse_qsl + from urlparse import parse_qsl, urlsplit str = unicode basestring = basestring @@ -33,7 +33,7 @@ if is_py2: elif is_py3: - from urllib.parse import urlencode, quote_plus, parse_qsl + from urllib.parse import urlencode, quote_plus, parse_qsl, urlsplit str = str basestring = (str, bytes) diff --git a/src/twython/endpoints.py b/src/twython/endpoints.py index ef1af810..f7e0a5a6 100644 --- a/src/twython/endpoints.py +++ b/src/twython/endpoints.py @@ -10,8 +10,8 @@ as a keyword argument. e.g. Twython.retweet(id=12345) -This map is organized the order functions are documented at: -https://dev.twitter.com/docs/api/1.1 +Official documentation for Twitter API endpoints can be found at: +https://developer.twitter.com/en/docs/api-reference-index """ import json @@ -34,7 +34,7 @@ class EndpointsMixin(object): @screen_name) for the authenticating user. Docs: - https://dev.twitter.com/docs/api/1.1/get/statuses/mentions_timeline + https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-mentions_timeline """ return self.get('statuses/mentions_timeline', params=params) @@ -42,9 +42,10 @@ class EndpointsMixin(object): def get_user_timeline(self, **params): """Returns a collection of the most recent Tweets posted by the user - indicated by the screen_name or user_id parameters. + indicated by the ``screen_name`` or ``user_id`` parameters. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline + Docs: + https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline """ return self.get('statuses/user_timeline', params=params) @@ -54,7 +55,8 @@ class EndpointsMixin(object): """Returns a collection of the most recent Tweets and retweets posted by the authenticating user and the users they follow. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline + Docs: + https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline """ return self.get('statuses/home_timeline', params=params) @@ -64,7 +66,8 @@ class EndpointsMixin(object): """Returns the most recent tweets authored by the authenticating user that have been retweeted by others. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweets_of_me + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweets_of_me """ return self.get('statuses/retweets_of_me', params=params) @@ -74,34 +77,38 @@ class EndpointsMixin(object): def get_retweets(self, **params): """Returns up to 100 of the first retweets of a given tweet. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweets/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id """ return self.get('statuses/retweets/%s' % params.get('id'), params=params) def show_status(self, **params): - """Returns a single Tweet, specified by the id parameter + """Returns a single Tweet, specified by the ``id`` parameter - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id """ return self.get('statuses/show/%s' % params.get('id'), params=params) def lookup_status(self, **params): """Returns fully-hydrated tweet objects for up to 100 tweets per - request, as specified by comma-separated values passed to the id + request, as specified by comma-separated values passed to the ``id`` parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/lookup + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-lookup """ return self.post('statuses/lookup', params=params) def destroy_status(self, **params): - """Destroys the status specified by the required ID parameter + """Destroys the status specified by the required ``id`` parameter - Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/destroy/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id """ return self.post('statuses/destroy/%s' % params.get('id')) @@ -109,15 +116,17 @@ class EndpointsMixin(object): def update_status(self, **params): """Updates the authenticating user's current status, also known as tweeting - Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/update + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update """ return self.post('statuses/update', params=params) def retweet(self, **params): - """Retweets a tweet specified by the id parameter + """Retweets a tweet specified by the ``id`` parameter - Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id """ return self.post('statuses/retweet/%s' % params.get('id')) @@ -127,7 +136,7 @@ class EndpointsMixin(object): for upload. In other words, it creates a Tweet with a picture attached. Docs: - https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update_with_media """ warnings.warn( @@ -140,12 +149,13 @@ class EndpointsMixin(object): def upload_media(self, **params): """Uploads media file to Twitter servers. The file will be available to be attached to a status for 60 minutes. To attach to a update, pass a list of returned media ids - to the 'update_status' method using the 'media_ids' param. + to the :meth:`update_status` method using the ``media_ids`` param. Docs: - https://dev.twitter.com/rest/reference/post/media/upload + https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload + """ - # https://dev.twitter.com/rest/reference/get/media/upload-status + # https://developer.twitter.com/en/docs/media/upload-media/api-reference/get-media-upload-status if params and params.get('command', '') == 'STATUS': return self.get('https://upload.twitter.com/1.1/media/upload.json', params=params) @@ -153,7 +163,10 @@ class EndpointsMixin(object): def create_metadata(self, **params): """ Adds metadata to a media element, such as image descriptions for visually impaired. - Docs: https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create + + Docs: + https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create + """ params = json.dumps(params) return self.post("https://upload.twitter.com/1.1/media/metadata/create.json", params=params) @@ -161,7 +174,7 @@ class EndpointsMixin(object): def upload_video(self, media, media_type, media_category=None, size=None, check_progress=False): """Uploads video file to Twitter servers in chunks. The file will be available to be attached to a status for 60 minutes. To attach to a update, pass a list of returned media ids - to the 'update_status' method using the 'media_ids' param. + to the :meth:`update_status` method using the ``media_ids`` param. Upload happens in 3 stages: - INIT call with size of media to be uploaded(in bytes). If this is more than 15mb, twitter will return error. @@ -171,7 +184,8 @@ class EndpointsMixin(object): Twitter media upload api expects each chunk to be not more than 5mb. We are sending chunk of 1mb each. Docs: - https://dev.twitter.com/rest/public/uploading-media#chunkedupload + https://developer.twitter.com/en/docs/media/upload-media/uploading-media/chunked-media-upload + """ upload_url = 'https://upload.twitter.com/1.1/media/upload.json' if not size: @@ -216,7 +230,7 @@ class EndpointsMixin(object): response = self.post(upload_url, params=params) - # Only get the status if explicity asked to + # Only get the status if explicity asked to # Default to False if check_progress: @@ -250,16 +264,18 @@ class EndpointsMixin(object): """Returns information allowing the creation of an embedded representation of a Tweet on third party sites. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/oembed + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed """ return self.get('statuses/oembed', params=params) def get_retweeters_ids(self, **params): """Returns a collection of up to 100 user IDs belonging to users who - have retweeted the tweet specified by the id parameter. + have retweeted the tweet specified by the ``id`` parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweeters/ids + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweeters-ids """ return self.get('statuses/retweeters/ids', params=params) @@ -270,7 +286,8 @@ class EndpointsMixin(object): def search(self, **params): """Returns a collection of relevant Tweets matching a specified query. - Docs: https://dev.twitter.com/docs/api/1.1/get/search/tweets + Docs: + https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets """ return self.get('search/tweets', params=params) @@ -282,7 +299,8 @@ class EndpointsMixin(object): def get_direct_messages(self, **params): """Returns the 20 most recent direct messages sent to the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events """ return self.get('direct_messages/events/list', params=params) @@ -291,24 +309,27 @@ class EndpointsMixin(object): def get_sent_messages(self, **params): """Returns the 20 most recent direct messages sent by the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages/sent + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-sent-message """ return self.get('direct_messages/sent', params=params) get_sent_messages.iter_mode = 'id' def get_direct_message(self, **params): - """Returns a single direct message, specified by an id parameter. + """Returns a single direct message, specified by an ``id`` parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages/show + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-event """ return self.get('direct_messages/events/show', params=params) def destroy_direct_message(self, **params): - """Destroys the direct message specified in the required id parameter + """Destroys the direct message specified in the required ``id`` parameter - Docs: https://dev.twitter.com/docs/api/1.1/post/direct_messages/destroy + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message """ return self.post('direct_messages/destroy', params=params) @@ -317,10 +338,11 @@ class EndpointsMixin(object): """Sends a new direct message to the specified user from the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/direct_messages/new + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event """ - return self.post('direct_messages/events/new', params=params, encode_json=True) + return self.post('direct_messages/events/new', params=params, json_encoded=True) # Friends & Followers def get_user_ids_of_blocked_retweets(self, **params): @@ -328,7 +350,7 @@ class EndpointsMixin(object): user does not want to receive retweets from. Docs: - https://dev.twitter.com/docs/api/1.1/get/friendships/no_retweets/ids + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-no_retweets-ids """ return self.get('friendships/no_retweets/ids', params=params) @@ -337,7 +359,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user IDs for every user the specified user is following (otherwise known as their "friends"). - Docs: https://dev.twitter.com/docs/api/1.1/get/friends/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-ids """ return self.get('friends/ids', params=params) @@ -348,7 +371,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user IDs for every user following the specified user. - Docs: https://dev.twitter.com/docs/api/1.1/get/followers/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids """ return self.get('followers/ids', params=params) @@ -357,9 +381,10 @@ class EndpointsMixin(object): def lookup_friendships(self, **params): """Returns the relationships of the authenticating user to the - comma-separated list of up to 100 screen_names or user_ids provided. + comma-separated list of up to 100 ``screen_names`` or ``user_ids`` provided. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/lookup + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-lookup """ return self.get('friendships/lookup', params=params) @@ -368,7 +393,8 @@ class EndpointsMixin(object): """Returns a collection of numeric IDs for every user who has a pending request to follow the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/incoming + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-incoming """ return self.get('friendships/incoming', params=params) @@ -379,7 +405,8 @@ class EndpointsMixin(object): """Returns a collection of numeric IDs for every protected user for whom the authenticating user has a pending follow request. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/outgoing + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-outgoing """ return self.get('friendships/outgoing', params=params) @@ -388,18 +415,20 @@ class EndpointsMixin(object): def create_friendship(self, **params): """Allows the authenticating users to follow the user specified - in the ID parameter. + in the ``id`` parameter. - Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-create """ return self.post('friendships/create', params=params) def destroy_friendship(self, **params): """Allows the authenticating user to unfollow the user specified - in the ID parameter. + in the ``id`` parameter. - Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy """ return self.post('friendships/destroy', params=params) @@ -408,7 +437,8 @@ class EndpointsMixin(object): """Allows one to enable or disable retweets and device notifications from the specified user. - Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/update + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-update """ return self.post('friendships/update', params=params) @@ -417,7 +447,8 @@ class EndpointsMixin(object): """Returns detailed information about the relationship between two arbitrary users. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-show """ return self.get('friendships/show', params=params) @@ -426,7 +457,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user objects for every user the specified user is following (otherwise known as their "friends"). - Docs: https://dev.twitter.com/docs/api/1.1/get/friends/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-list """ return self.get('friends/list', params=params) @@ -437,7 +469,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user objects for users following the specified user. - Docs: https://dev.twitter.com/docs/api/1.1/get/followers/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-list """ return self.get('followers/list', params=params) @@ -449,7 +482,8 @@ class EndpointsMixin(object): """Returns settings (including current trend, geo and sleep time information) for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/account/settings + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-settings """ return self.get('account/settings', params=params) @@ -460,7 +494,7 @@ class EndpointsMixin(object): code and an error message if not. Docs: - https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials """ return self.get('account/verify_credentials', params=params) @@ -468,7 +502,8 @@ class EndpointsMixin(object): def update_account_settings(self, **params): """Updates the authenticating user's settings. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/settings + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-settings """ return self.post('account/settings', params=params) @@ -486,7 +521,8 @@ class EndpointsMixin(object): """Sets values that users are able to set under the "Account" tab of their settings page. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile """ return self.post('account/update_profile', params=params) @@ -495,26 +531,35 @@ class EndpointsMixin(object): """Updates the authenticating user's profile background image. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_background_image """ return self.post('account/update_profile_banner', params=params) - def update_profile_colors(self, **params): + def update_profile_colors(self, **params): # pragma: no cover """Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. + This method is deprecated, replaced by the ``profile_link_color`` + parameter to :meth:`update_profile`. + Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile """ + warnings.warn( + 'This method is deprecated. You should use the' + ' profile_link_color parameter in Twython.update_profile instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) return self.post('account/update_profile_colors', params=params) def update_profile_image(self, **params): # pragma: no cover """Updates the authenticating user's profile image. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image """ return self.post('account/update_profile_image', params=params) @@ -523,7 +568,8 @@ class EndpointsMixin(object): """Returns a collection of user objects that the authenticating user is blocking. - Docs: https://dev.twitter.com/docs/api/1.1/get/blocks/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-list """ return self.get('blocks/list', params=params) @@ -533,7 +579,8 @@ class EndpointsMixin(object): def list_block_ids(self, **params): """Returns an array of numeric user ids the authenticating user is blocking. - Docs: https://dev.twitter.com/docs/api/1.1/get/blocks/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-ids """ return self.get('blocks/ids', params=params) @@ -543,35 +590,39 @@ class EndpointsMixin(object): def create_block(self, **params): """Blocks the specified user from following the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/blocks/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-create """ return self.post('blocks/create', params=params) def destroy_block(self, **params): - """Un-blocks the user specified in the ID parameter for the + """Un-blocks the user specified in the ``id`` parameter for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/blocks/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-destroy """ return self.post('blocks/destroy', params=params) def lookup_user(self, **params): """Returns fully-hydrated user objects for up to 100 users per request, - as specified by comma-separated values passed to the user_id and/or - screen_name parameters. + as specified by comma-separated values passed to the ``user_id`` and/or + ``screen_name`` parameters. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/lookup + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-lookup """ return self.get('users/lookup', params=params) def show_user(self, **params): """Returns a variety of information about the user specified by the - required user_id or screen_name parameter. + required ``user_id`` or ``screen_name`` parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show """ return self.get('users/show', params=params) @@ -580,7 +631,8 @@ class EndpointsMixin(object): """Provides a simple, relevance-based search interface to public user accounts on Twitter. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/search + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-search """ return self.get('users/search', params=params) @@ -606,7 +658,7 @@ class EndpointsMixin(object): Returns HTTP 200 upon success. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/remove_profile_banner + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-remove_profile_banner """ return self.post('account/remove_profile_banner', params=params) @@ -615,7 +667,7 @@ class EndpointsMixin(object): """Uploads a profile banner on behalf of the authenticating user. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_banner """ return self.post('account/update_profile_background_image', @@ -625,7 +677,8 @@ class EndpointsMixin(object): """Returns a map of the available size variations of the specified user's profile banner. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/profile_banner + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-users-profile_banner """ return self.get('users/profile_banner', params=params) @@ -634,7 +687,8 @@ class EndpointsMixin(object): """Returns a collection of user objects that the authenticating user is muting. - Docs: https://dev.twitter.com/docs/api/1.1/get/mutes/users/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-mutes-users-list """ return self.get('mutes/users/list', params=params) @@ -645,7 +699,8 @@ class EndpointsMixin(object): """Returns an array of numeric user ids the authenticating user is muting. - Docs: https://dev.twitter.com/docs/api/1.1/get/mutes/users/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-mutes-users-ids """ return self.get('mutes/users/ids', params=params) @@ -656,16 +711,18 @@ class EndpointsMixin(object): """Mutes the specified user, preventing their tweets appearing in the authenticating user's timeline. - Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-create """ return self.post('mutes/users/create', params=params) def destroy_mute(self, **params): - """Un-mutes the user specified in the user or ID parameter for + """Un-mutes the user specified in the user or ``id`` parameter for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-destroy """ return self.post('mutes/users/destroy', params=params) @@ -675,7 +732,7 @@ class EndpointsMixin(object): """Access the users in a given category of the Twitter suggested user list. Docs: - https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions-slug """ return self.get('users/suggestions/%s' % params.get('slug'), @@ -684,7 +741,8 @@ class EndpointsMixin(object): def get_user_suggestions(self, **params): """Access to Twitter's suggested user list. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/suggestions + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions """ return self.get('users/suggestions', params=params) @@ -695,7 +753,7 @@ class EndpointsMixin(object): user. Docs: - https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug/members + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions-slug-members """ return self.get('users/suggestions/%s/members' % params.get('slug'), @@ -706,26 +764,29 @@ class EndpointsMixin(object): """Returns the 20 most recent Tweets favorited by the authenticating or specified user. - Docs: https://dev.twitter.com/docs/api/1.1/get/favorites/list + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-favorites-list """ return self.get('favorites/list', params=params) get_favorites.iter_mode = 'id' def destroy_favorite(self, **params): - """Un-favorites the status specified in the ID parameter as the + """Un-favorites the status specified in the ``id`` parameter as the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/destroy + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-favorites-destroy """ return self.post('favorites/destroy', params=params) def create_favorite(self, **params): - """Favorites the status specified in the ID parameter as the + """Favorites the status specified in the ``id`` parameter as the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/create + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-favorites-create """ return self.post('favorites/create', params=params) @@ -735,7 +796,8 @@ class EndpointsMixin(object): """Returns all lists the authenticating or specified user subscribes to, including their own. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list """ return self.get('lists/list', params=params) @@ -743,7 +805,8 @@ class EndpointsMixin(object): def get_list_statuses(self, **params): """Returns a timeline of tweets authored by members of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/statuses + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-statuses """ return self.get('lists/statuses', params=params) @@ -752,7 +815,8 @@ class EndpointsMixin(object): def delete_list_member(self, **params): """Removes the specified member from the list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-destroy """ return self.post('lists/members/destroy', params=params) @@ -760,7 +824,8 @@ class EndpointsMixin(object): def get_list_memberships(self, **params): """Returns the lists the specified user has been added to. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/memberships + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-memberships """ return self.get('lists/memberships', params=params) @@ -770,7 +835,8 @@ class EndpointsMixin(object): def get_list_subscribers(self, **params): """Returns the subscribers of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscribers """ return self.get('lists/subscribers', params=params) @@ -781,7 +847,7 @@ class EndpointsMixin(object): """Subscribes the authenticated user to the specified list. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-subscribers-create """ return self.post('lists/subscribers/create', params=params) @@ -789,7 +855,8 @@ class EndpointsMixin(object): def is_list_subscriber(self, **params): """Check if the specified user is a subscriber of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscribers-show """ return self.get('lists/subscribers/show', params=params) @@ -798,7 +865,7 @@ class EndpointsMixin(object): """Unsubscribes the authenticated user from the specified list. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-subscribers-destroy """ return self.post('lists/subscribers/destroy', params=params) @@ -808,7 +875,7 @@ class EndpointsMixin(object): list of member ids or screen names. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-create_all """ return self.post('lists/members/create_all', params=params) @@ -816,7 +883,8 @@ class EndpointsMixin(object): def is_list_member(self, **params): """Check if the specified user is a member of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/members/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-members-show """ return self.get('lists/members/show', params=params) @@ -824,7 +892,8 @@ class EndpointsMixin(object): def get_list_members(self, **params): """Returns the members of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/members + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-members """ return self.get('lists/members', params=params) @@ -834,7 +903,8 @@ class EndpointsMixin(object): def add_list_member(self, **params): """Add a member to a list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-create """ return self.post('lists/members/create', params=params) @@ -842,7 +912,8 @@ class EndpointsMixin(object): def delete_list(self, **params): """Deletes the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-destroy """ return self.post('lists/destroy', params=params) @@ -850,7 +921,8 @@ class EndpointsMixin(object): def update_list(self, **params): """Updates the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/update + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-update """ return self.post('lists/update', params=params) @@ -858,7 +930,8 @@ class EndpointsMixin(object): def create_list(self, **params): """Creates a new list for the authenticated user. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-create """ return self.post('lists/create', params=params) @@ -866,7 +939,8 @@ class EndpointsMixin(object): def get_specific_list(self, **params): """Returns the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-show """ return self.get('lists/show', params=params) @@ -874,7 +948,8 @@ class EndpointsMixin(object): def get_list_subscriptions(self, **params): """Obtain a collection of the lists the specified user is subscribed to. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscriptions + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscriptions """ return self.get('lists/subscriptions', params=params) @@ -886,7 +961,7 @@ class EndpointsMixin(object): comma-separated list of member ids or screen names. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-destroy_all """ return self.post('lists/members/destroy_all', params=params) @@ -894,7 +969,8 @@ class EndpointsMixin(object): def show_owned_lists(self, **params): """Returns the lists owned by the specified Twitter user. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/ownerships + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships """ return self.get('lists/ownerships', params=params) @@ -905,16 +981,17 @@ class EndpointsMixin(object): def get_saved_searches(self, **params): """Returns the authenticated user's saved search queries. - Docs: https://dev.twitter.com/docs/api/1.1/get/saved_searches/list + Docs: + https://developer.twitter.com/en/docs/tweets/search/api-reference/get-saved_searches-list """ return self.get('saved_searches/list', params=params) def show_saved_search(self, **params): - """Retrieve the information for the saved search represented by the given id. + """Retrieve the information for the saved search represented by the given ``id``. Docs: - https://dev.twitter.com/docs/api/1.1/get/saved_searches/show/%3Aid + https://developer.twitter.com/en/docs/tweets/search/api-reference/get-saved_searches-show-id """ return self.get('saved_searches/show/%s' % params.get('id'), @@ -923,7 +1000,8 @@ class EndpointsMixin(object): def create_saved_search(self, **params): """Create a new saved search for the authenticated user. - Docs: https://dev.twitter.com/docs/api/1.1/post/saved_searches/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-create """ return self.post('saved_searches/create', params=params) @@ -932,7 +1010,7 @@ class EndpointsMixin(object): """Destroys a saved search for the authenticating user. Docs: - https://dev.twitter.com/docs/api/1.1/post/saved_searches/destroy/%3Aid + https://developer.twitter.com/en/docs/tweets/search/api-reference/post-saved_searches-destroy-id """ return self.post('saved_searches/destroy/%s' % params.get('id'), @@ -942,7 +1020,8 @@ class EndpointsMixin(object): def get_geo_info(self, **params): """Returns all the information about a known place. - Docs: https://dev.twitter.com/docs/api/1.1/get/geo/id/%3Aplace_id + Docs: + https://developer.twitter.com/en/docs/geo/place-information/api-reference/get-geo-id-place_id """ return self.get('geo/id/%s' % params.get('place_id'), params=params) @@ -951,7 +1030,8 @@ class EndpointsMixin(object): """Given a latitude and a longitude, searches for up to 20 places that can be used as a place_id when updating a status. - Docs: https://dev.twitter.com/docs/api/1.1/get/geo/reverse_geocode + Docs: + https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-reverse_geocode """ return self.get('geo/reverse_geocode', params=params) @@ -959,7 +1039,8 @@ class EndpointsMixin(object): def search_geo(self, **params): """Search for places that can be attached to a statuses/update. - Docs: https://dev.twitter.com/docs/api/1.1/get/geo/search + Docs: + https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-search """ return self.get('geo/search', params=params) @@ -985,7 +1066,8 @@ class EndpointsMixin(object): """Returns the top 10 trending topics for a specific WOEID, if trending information is available for it. - Docs: https://dev.twitter.com/docs/api/1.1/get/trends/place + Docs: + https://developer.twitter.com/en/docs/trends/trends-for-location/api-reference/get-trends-place """ return self.get('trends/place', params=params) @@ -993,7 +1075,8 @@ class EndpointsMixin(object): def get_available_trends(self, **params): """Returns the locations that Twitter has trending topic information for. - Docs: https://dev.twitter.com/docs/api/1.1/get/trends/available + Docs: + https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-available """ return self.get('trends/available', params=params) @@ -1002,7 +1085,8 @@ class EndpointsMixin(object): """Returns the locations that Twitter has trending topic information for, closest to a specified location. - Docs: https://dev.twitter.com/docs/api/1.1/get/trends/closest + Docs: + https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-closest """ return self.get('trends/closest', params=params) @@ -1011,7 +1095,8 @@ class EndpointsMixin(object): def report_spam(self, **params): # pragma: no cover """Report the specified user as a spam account to Twitter. - Docs: https://dev.twitter.com/docs/api/1.1/post/users/report_spam + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-users-report_spam """ return self.post('users/report_spam', params=params) @@ -1021,7 +1106,8 @@ class EndpointsMixin(object): """Allows a registered application to revoke an issued OAuth 2 Bearer Token by presenting its client credentials. - Docs: https://dev.twitter.com/docs/api/1.1/post/oauth2/invalidate_token + Docs: + https://developer.twitter.com/en/docs/basics/authentication/api-reference/invalidate_token """ return self.post('oauth2/invalidate_token', params=params) @@ -1030,7 +1116,8 @@ class EndpointsMixin(object): def get_twitter_configuration(self, **params): """Returns the current configuration used by Twitter - Docs: https://dev.twitter.com/docs/api/1.1/get/help/configuration + Docs: + https://developer.twitter.com/en/docs/developer-utilities/configuration/api-reference/get-help-configuration """ return self.get('help/configuration', params=params) @@ -1039,7 +1126,8 @@ class EndpointsMixin(object): """Returns the list of languages supported by Twitter along with their ISO 639-1 code. - Docs: https://dev.twitter.com/docs/api/1.1/get/help/languages + Docs: + https://developer.twitter.com/en/docs/developer-utilities/supported-languages/api-reference/get-help-languages """ return self.get('help/languages', params=params) @@ -1047,7 +1135,8 @@ class EndpointsMixin(object): def get_privacy_policy(self, **params): """Returns Twitter's Privacy Policy - Docs: https://dev.twitter.com/docs/api/1.1/get/help/privacy + Docs: + https://developer.twitter.com/en/docs/developer-utilities/privacy-policy/api-reference/get-help-privacy """ return self.get('help/privacy', params=params) @@ -1055,7 +1144,8 @@ class EndpointsMixin(object): def get_tos(self, **params): """Return the Twitter Terms of Service - Docs: https://dev.twitter.com/docs/api/1.1/get/help/tos + Docs: + https://developer.twitter.com/en/docs/developer-utilities/terms-of-service/api-reference/get-help-tos """ return self.get('help/tos', params=params) @@ -1065,13 +1155,13 @@ class EndpointsMixin(object): specified resource families. Docs: - https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status + https://developer.twitter.com/en/docs/developer-utilities/rate-limit-status/api-reference/get-application-rate_limit_status """ return self.get('application/rate_limit_status', params=params) -# from https://dev.twitter.com/docs/error-codes-responses +# from https://developer.twitter.com/en/docs/ads/general/guides/response-codes TWITTER_HTTP_STATUS_CODE = { 200: ('OK', 'Success!'), 304: ('Not Modified', 'There was no new data to return.'), diff --git a/src/twython/streaming/api.py b/src/twython/streaming/api.py index 47678e47..0195f380 100644 --- a/src/twython/streaming/api.py +++ b/src/twython/streaming/api.py @@ -169,9 +169,9 @@ class TwythonStreamer(object): Returns True if other handlers for this message should be invoked. Feel free to override this to handle your streaming data how you - want it handled. - See https://dev.twitter.com/docs/streaming-apis/messages for messages - sent along in stream responses. + want it handled. See + https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/streaming-message-types + for messages sent along in stream responses. :param data: data recieved from the stream :type data: dict diff --git a/src/twython/streaming/types.py b/src/twython/streaming/types.py index aa6b9adb..81c5c07f 100644 --- a/src/twython/streaming/types.py +++ b/src/twython/streaming/types.py @@ -44,9 +44,10 @@ class TwythonStreamerTypes(object): class TwythonStreamerTypesStatuses(object): """Class for different statuses endpoints - Available so TwythonStreamer.statuses.filter() is available. - Just a bit cleaner than TwythonStreamer.statuses_filter(), - statuses_sample(), etc. all being single methods in TwythonStreamer + Available so :meth:`TwythonStreamer.statuses.filter()` is available. + Just a bit cleaner than :meth:`TwythonStreamer.statuses_filter()`, + :meth:`statuses_sample()`, etc. all being single methods in + :class:`TwythonStreamer`. """ def __init__(self, streamer): @@ -59,7 +60,7 @@ class TwythonStreamerTypesStatuses(object): :param \*\*params: Parameters to send with your stream request Accepted params found at: - https://dev.twitter.com/docs/api/1.1/post/statuses/filter + https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter """ url = 'https://stream.twitter.com/%s/statuses/filter.json' \ % self.streamer.api_version @@ -71,7 +72,7 @@ class TwythonStreamerTypesStatuses(object): :param \*\*params: Parameters to send with your stream request Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/statuses/sample + https://developer.twitter.com/en/docs/tweets/sample-realtime/api-reference/get-statuses-sample """ url = 'https://stream.twitter.com/%s/statuses/sample.json' \ % self.streamer.api_version @@ -95,7 +96,7 @@ class TwythonStreamerTypesStatuses(object): :param \*\*params: Parameters to send with your stream request Accepted params found at: - https://dev.twitter.com/docs/api/1.1/post/statuses/filter + https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter """ self.params = params @@ -104,4 +105,4 @@ class TwythonStreamerTypesStatuses(object): url = 'https://stream.twitter.com/%s/statuses/filter.json' \ % self.streamer.api_version - self.streamer._request(url, 'POST', params=self.params) + self.streamer._request(url, 'POST', params=self.params) From 7748b4bb5dd8693acd2a4a1232f20b40cd63ce3b Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 22 Sep 2018 13:03:21 -0500 Subject: [PATCH 52/75] Added attempt to update setuptools in AppVeyor script --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 929f00fc..c86d3926 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -46,7 +46,7 @@ install: # Upgrade to the latest version of pip to avoid it displaying warnings # about it being out of date. - - "python -m pip install --upgrade pip" + - "python -m pip install --upgrade pip setuptools" # Install the build dependencies of the project. If some dependencies contain # compiled extensions and are not provided as pre-built wheel packages, From a6a651d6f70dd240d08bb98b549cc7748dfd0662 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 22 Sep 2018 13:04:27 -0500 Subject: [PATCH 53/75] Install py2exe_py2 in appveyor script only. Removed it from requirements. Closes #266 --- appveyor.yml | 2 +- requirements.txt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c86d3926..851fbd31 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -53,7 +53,7 @@ install: # pip will build them from source using the MSVC compiler matching the # target Python version and architecture - "%CMD_IN_ENV% pip install -r requirements.txt" - - "%CMD_IN_ENV% pip install pyenchant" + - "%CMD_IN_ENV% pip install pyenchant py2exe_py2" build_script: # Build documentation at first, so setup.py won't fail when copying everything. diff --git a/requirements.txt b/requirements.txt index a4eb000e..65691da8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,4 @@ chardet urllib3 youtube-dl python-vlc -pywin32 -py2exe_py2 \ No newline at end of file +pywin32 \ No newline at end of file From ca3f8779b86f6c4f12659596758385bacce15f5e Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 22 Sep 2018 21:55:19 -0500 Subject: [PATCH 54/75] Separated twitter buffers from generic buffers code --- src/controller/buffers/__init__.py | 1 + src/controller/buffers/baseBuffers.py | 199 +++++++++++ .../twitterBuffers.py} | 310 +++--------------- src/controller/mainController.py | 96 +++--- 4 files changed, 288 insertions(+), 318 deletions(-) create mode 100644 src/controller/buffers/__init__.py create mode 100644 src/controller/buffers/baseBuffers.py rename src/controller/{buffersController.py => buffers/twitterBuffers.py} (85%) diff --git a/src/controller/buffers/__init__.py b/src/controller/buffers/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/src/controller/buffers/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/controller/buffers/baseBuffers.py b/src/controller/buffers/baseBuffers.py new file mode 100644 index 00000000..fe469205 --- /dev/null +++ b/src/controller/buffers/baseBuffers.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +import wx +import output +import config +import sound +import widgetUtils +import logging +from pubsub import pub +from wxUI import buffers + +log = logging.getLogger("controller.buffers") + +def _items_exist(function): + """ A decorator to execute a function only if the selected buffer contains at least one item.""" + def function_(self, *args, **kwargs): + if self.buffer.list.get_count() > 0: + function(self, *args, **kwargs) + return function_ + +class buffer(object): + """ A basic buffer object. This should be the base class for all other derived buffers.""" + + def __init__(self, parent=None, function=None, session=None, *args, **kwargs): + """Inits the main controller for this buffer: + @ parent wx.Treebook object: Container where we will put this buffer. + @ function str or None: function to be called periodically and update items on this buffer. + @ session sessionmanager.session object or None: Session handler for settings, database and Twitter access. + """ + super(buffer, self).__init__() + self.function = function + # Compose_function will be used to render an object on this buffer. Normally, signature is as follows: + # compose_function(item, db, relative_times, show_screen_names=False, session=None) + # Compose functions will be defined in every buffer if items are different than tweets. + # Read more about compose functions in twitter/compose.py. + self.compose_function = None + self.args = args + self.kwargs = kwargs + # This will be used as a reference to the wx.Panel object wich stores the buffer GUI. + self.buffer = None + # This should countains the account associated to this buffer. + self.account = "" + # This controls wether the start_stream function should be called when starting the program. + self.needs_init = True + # if this is set to False, the buffer will be ignored on the invisible interface. + self.invisible = False + # Control variable, used to track time of execution for calls to start_stream. + self.execution_time = 0 + + def clear_list(self): + pass + + def get_event(self, ev): + """ Catches key presses in the WX interface and generate the corresponding event names.""" + if ev.GetKeyCode() == wx.WXK_RETURN and ev.ControlDown(): event = "audio" + elif ev.GetKeyCode() == wx.WXK_RETURN: event = "url" + elif ev.GetKeyCode() == wx.WXK_F5: event = "volume_down" + elif ev.GetKeyCode() == wx.WXK_F6: event = "volume_up" + elif ev.GetKeyCode() == wx.WXK_DELETE and ev.ShiftDown(): event = "clear_list" + elif ev.GetKeyCode() == wx.WXK_DELETE: event = "destroy_status" + else: + event = None + ev.Skip() + if event != None: + try: + getattr(self, event)() + except AttributeError: + pass + + def volume_down(self): + if self.session.settings["sound"]["volume"] > 0.0: + if self.session.settings["sound"]["volume"] <= 0.05: + self.session.settings["sound"]["volume"] = 0.0 + else: + self.session.settings["sound"]["volume"] -=0.05 + sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100.0)) + self.session.sound.play("volume_changed.ogg") + self.session.settings.write() + + def volume_up(self): + if self.session.settings["sound"]["volume"] < 1.0: + if self.session.settings["sound"]["volume"] >= 0.95: + self.session.settings["sound"]["volume"] = 1.0 + else: + self.session.settings["sound"]["volume"] +=0.05 + sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100)) + self.session.sound.play("volume_changed.ogg") + self.session.settings.write() + + def start_stream(self, mandatory=False, play_sound=True): + pass + + def get_more_items(self): + output.speak(_(u"This action is not supported for this buffer"), True) + + def put_items_on_list(self, items): + pass + + def remove_buffer(self): + return False + + def remove_item(self, item): + f = self.buffer.list.get_selected() + self.buffer.list.remove_item(item) + self.buffer.list.select_item(f) + + def bind_events(self): + pass + + def get_object(self): + return self.buffer + + def get_message(self): + pass + + def set_list_position(self, reversed=False): + if reversed == False: + self.buffer.list.select_item(-1) + else: + self.buffer.list.select_item(0) + + def reply(self): + pass + + def send_message(self): + pass + + def share_item(self): + pass + + def destroy_status(self): + pass + + def post_status(self, *args, **kwargs): + pass + + def save_positions(self): + try: + self.session.db[self.name+"_pos"]=self.buffer.list.get_selected() + except AttributeError: + pass + +class accountPanel(buffer): + def __init__(self, parent, name, account, account_id): + super(accountPanel, self).__init__(parent, None, name) + log.debug("Initializing buffer %s, account %s" % (name, account,)) + self.buffer = buffers.accountPanel(parent, name) + self.type = self.buffer.type + self.compose_function = None + self.session = None + self.needs_init = False + self.account = account + self.buffer.account = account + self.name = name + self.account_id = account_id + + def setup_account(self): + widgetUtils.connect_event(self.buffer, widgetUtils.CHECKBOX, self.autostart, menuitem=self.buffer.autostart_account) + if self.account_id in config.app["sessions"]["ignored_sessions"]: + self.buffer.change_autostart(False) + else: + self.buffer.change_autostart(True) + if not hasattr(self, "logged"): + self.buffer.change_login(login=False) + widgetUtils.connect_event(self.buffer.login, widgetUtils.BUTTON_PRESSED, self.logout) + else: + self.buffer.change_login(login=True) + widgetUtils.connect_event(self.buffer.login, widgetUtils.BUTTON_PRESSED, self.login) + + def login(self, *args, **kwargs): + del self.logged + self.setup_account() + pub.sendMessage("login", session_id=self.account_id) + + def logout(self, *args, **kwargs): + self.logged = False + self.setup_account() + pub.sendMessage("logout", session_id=self.account_id) + + def autostart(self, *args, **kwargs): + if self.account_id in config.app["sessions"]["ignored_sessions"]: + self.buffer.change_autostart(True) + config.app["sessions"]["ignored_sessions"].remove(self.account_id) + else: + self.buffer.change_autostart(False) + config.app["sessions"]["ignored_sessions"].append(self.account_id) + config.app.write() + +class emptyPanel(buffer): + def __init__(self, parent, name, account): + super(emptyPanel, self).__init__(parent=parent) + log.debug("Initializing buffer %s, account %s" % (name, account,)) + self.buffer = buffers.emptyPanel(parent, name) + self.type = self.buffer.type + self.compose_function = None + self.account = account + self.buffer.account = account + self.name = name + self.session = None + self.needs_init = True diff --git a/src/controller/buffersController.py b/src/controller/buffers/twitterBuffers.py similarity index 85% rename from src/controller/buffersController.py rename to src/controller/buffers/twitterBuffers.py index 57d4e84a..225de733 100644 --- a/src/controller/buffersController.py +++ b/src/controller/buffers/twitterBuffers.py @@ -4,11 +4,11 @@ import platform if platform.system() == "Windows": import wx from wxUI import buffers, dialogs, commonMessageDialogs, menus - import user + from controller import user elif platform.system() == "Linux": from gi.repository import Gtk from gtkUI import buffers, dialogs, commonMessageDialogs -import messages +from controller import messages import widgetUtils import arrow import webbrowser @@ -18,6 +18,7 @@ import sound import languageHandler import logging import youtube_utils +from controller.buffers import baseBuffers from sessions.twitter import compose, utils from mysc.thread_utils import call_threaded from twython import TwythonError @@ -33,121 +34,36 @@ def _tweets_exist(function): function(self, *args, **kwargs) return function_ -class bufferController(object): - """ A basic buffer object. This should be the base class for all other derived buffers.""" - - def __init__(self, parent=None, function=None, session=None, *args, **kwargs): - """Inits the main controller for this buffer: - @ parent wx.Treebook object: Container where we will put this buffer. - @ function str or None: function to be called periodically and update items on this buffer. - @ session sessionmanager.session object or None: Session handler for settings, database and Twitter access. - """ - super(bufferController, self).__init__() - self.function = function - # Compose_function will be used to render an object on this buffer. Normally, signature is as follows: - # compose_function(item, db, relative_times, show_screen_names=False, session=None) - # Compose functions will be defined in every buffer if items are different than tweets. - # Read more about compose functions in twitter/compose.py. - self.compose_function = None - self.args = args - self.kwargs = kwargs - # This will be used as a reference to the wx.Panel object wich stores the buffer GUI. - self.buffer = None - # This should countains the account associated to this buffer. - self.account = "" - # This controls wether the start_stream function should be called when starting the program. - self.needs_init = True - # if this is set to False, the buffer will be ignored on the invisible interface. - self.invisible = False - # Control variable, used to track time of execution for calls to start_stream. - self.execution_time = 0 - - def clear_list(self): pass - - def get_event(self, ev): - """ Catches key presses in the WX interface and generate the corresponding event names.""" - if ev.GetKeyCode() == wx.WXK_RETURN and ev.ControlDown(): event = "audio" - elif ev.GetKeyCode() == wx.WXK_RETURN: event = "url" - elif ev.GetKeyCode() == wx.WXK_F5: event = "volume_down" - elif ev.GetKeyCode() == wx.WXK_F6: event = "volume_up" - elif ev.GetKeyCode() == wx.WXK_DELETE and ev.ShiftDown(): event = "clear_list" - elif ev.GetKeyCode() == wx.WXK_DELETE: event = "destroy_status" +class baseBufferController(baseBuffers.buffer): + def __init__(self, parent, function, name, sessionObject, account, sound=None, bufferType=None, compose_func="compose_tweet", *args, **kwargs): + super(baseBufferController, self).__init__(parent, function, *args, **kwargs) + log.debug("Initializing buffer %s, account %s" % (name, account,)) + if bufferType != None: + self.buffer = getattr(buffers, bufferType)(parent, name) else: - event = None - ev.Skip() - if event != None: + self.buffer = buffers.basePanel(parent, name) + self.invisible = True + self.name = name + self.type = self.buffer.type + self.session = sessionObject + self.compose_function = getattr(compose, compose_func) + log.debug("Compose_function: %s" % (self.compose_function,)) + self.account = account + self.buffer.account = account + self.bind_events() + self.sound = sound + if "-timeline" in self.name or "-favorite" in self.name: + self.finished_timeline = False + # Add a compatibility layer for username based timelines from config. + # ToDo: Remove this in some new versions of the client, when user ID timelines become mandatory. try: - getattr(self, event)() - except AttributeError: - pass - - def volume_down(self): - if self.session.settings["sound"]["volume"] > 0.0: - if self.session.settings["sound"]["volume"] <= 0.05: - self.session.settings["sound"]["volume"] = 0.0 - else: - self.session.settings["sound"]["volume"] -=0.05 - sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100.0)) - self.session.sound.play("volume_changed.ogg") - self.session.settings.write() + int(self.kwargs["user_id"]) + except ValueError: + self.is_screen_name = True + self.kwargs["screen_name"] = self.kwargs["user_id"] + self.kwargs.pop("user_id") - def volume_up(self): - if self.session.settings["sound"]["volume"] < 1.0: - if self.session.settings["sound"]["volume"] >= 0.95: - self.session.settings["sound"]["volume"] = 1.0 - else: - self.session.settings["sound"]["volume"] +=0.05 - sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100)) - self.session.sound.play("volume_changed.ogg") - self.session.settings.write() - - def start_stream(self, mandatory=False, play_sound=True): -# if mandatory == True: -# output.speak(_(u"Unable to update this buffer.")) - pass - - def get_more_items(self): - output.speak(_(u"This action is not supported for this buffer"), True) - - def put_items_on_list(self, items): - pass - - def remove_buffer(self): - return False - - def remove_item(self, item): - f = self.buffer.list.get_selected() - self.buffer.list.remove_item(item) - self.buffer.list.select_item(f) - - def bind_events(self): - pass - - def get_object(self): - return self.buffer - - def get_message(self): - pass - - def set_list_position(self, reversed=False): - if reversed == False: - self.buffer.list.select_item(-1) - else: - self.buffer.list.select_item(0) - - def reply(self): - pass - - def direct_message(self): - pass - - def retweet(self): - pass - - def destroy_status(self): - pass - - def post_tweet(self, *args, **kwargs): + def post_status(self, *args, **kwargs): title = _(u"Tweet") caption = _(u"Write the tweet here") tweet = messages.tweet(self.session, title, caption, "") @@ -179,100 +95,6 @@ class bufferController(object): media_ids.append(img["media_id"]) self.session.twitter.update_status(status=text, media_ids=media_ids) - def save_positions(self): - try: - self.session.db[self.name+"_pos"]=self.buffer.list.get_selected() - except AttributeError: - pass - -class accountPanel(bufferController): - def __init__(self, parent, name, account, account_id): - super(accountPanel, self).__init__(parent, None, name) - log.debug("Initializing buffer %s, account %s" % (name, account,)) - self.buffer = buffers.accountPanel(parent, name) - self.type = self.buffer.type - self.compose_function = None - self.session = None - self.needs_init = False - self.account = account - self.buffer.account = account - self.name = name - self.account_id = account_id - - def setup_account(self): - widgetUtils.connect_event(self.buffer, widgetUtils.CHECKBOX, self.autostart, menuitem=self.buffer.autostart_account) - if self.account_id in config.app["sessions"]["ignored_sessions"]: - self.buffer.change_autostart(False) - else: - self.buffer.change_autostart(True) - if not hasattr(self, "logged"): - self.buffer.change_login(login=False) - widgetUtils.connect_event(self.buffer.login, widgetUtils.BUTTON_PRESSED, self.logout) - else: - self.buffer.change_login(login=True) - widgetUtils.connect_event(self.buffer.login, widgetUtils.BUTTON_PRESSED, self.login) - - def login(self, *args, **kwargs): - del self.logged - self.setup_account() - pub.sendMessage("login", session_id=self.account_id) - - def logout(self, *args, **kwargs): - self.logged = False - self.setup_account() - pub.sendMessage("logout", session_id=self.account_id) - - def autostart(self, *args, **kwargs): - if self.account_id in config.app["sessions"]["ignored_sessions"]: - self.buffer.change_autostart(True) - config.app["sessions"]["ignored_sessions"].remove(self.account_id) - else: - self.buffer.change_autostart(False) - config.app["sessions"]["ignored_sessions"].append(self.account_id) - config.app.write() - -class emptyPanel(bufferController): - def __init__(self, parent, name, account): - super(emptyPanel, self).__init__(parent=parent) - log.debug("Initializing buffer %s, account %s" % (name, account,)) - self.buffer = buffers.emptyPanel(parent, name) - self.type = self.buffer.type - self.compose_function = None - self.account = account - self.buffer.account = account - self.name = name - self.session = None - self.needs_init = True - -class baseBufferController(bufferController): - def __init__(self, parent, function, name, sessionObject, account, sound=None, bufferType=None, compose_func="compose_tweet", *args, **kwargs): - super(baseBufferController, self).__init__(parent, function, *args, **kwargs) - log.debug("Initializing buffer %s, account %s" % (name, account,)) - if bufferType != None: - self.buffer = getattr(buffers, bufferType)(parent, name) - else: - self.buffer = buffers.basePanel(parent, name) - self.invisible = True - self.name = name - self.type = self.buffer.type - self.session = sessionObject - self.compose_function = getattr(compose, compose_func) - log.debug("Compose_function: %s" % (self.compose_function,)) - self.account = account - self.buffer.account = account - self.bind_events() - self.sound = sound - if "-timeline" in self.name or "-favorite" in self.name: - self.finished_timeline = False - # Add a compatibility layer for username based timelines from config. - # ToDo: Remove this in some new versions of the client, when user ID timelines become mandatory. - try: - int(self.kwargs["user_id"]) - except ValueError: - self.is_screen_name = True - self.kwargs["screen_name"] = self.kwargs["user_id"] - self.kwargs.pop("user_id") - def get_formatted_message(self): if self.type == "dm" or self.name == "direct_messages": return self.compose_function(self.get_right_tweet(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session)[1] @@ -460,10 +282,10 @@ class baseBufferController(bufferController): log.debug("Binding events...") self.buffer.set_focus_function(self.onFocus) widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event) - widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_tweet, self.buffer.tweet) + widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.tweet) # if self.type == "baseBuffer": - widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.retweet, self.buffer.retweet) - widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.direct_message, self.buffer.dm) + widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.share_item, self.buffer.retweet) + widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.send_message, self.buffer.dm) widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply) # Replace for the correct way in other platforms. widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu) @@ -475,13 +297,13 @@ class baseBufferController(bufferController): menu = menus.sentPanelMenu() elif self.name == "direct_messages": menu = menus.dmPanelMenu() - widgetUtils.connect_event(menu, widgetUtils.MENU, self.direct_message, menuitem=menu.reply) + widgetUtils.connect_event(menu, widgetUtils.MENU, self.send_message, menuitem=menu.reply) widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions) else: menu = menus.basePanelMenu() widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply) widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions) - widgetUtils.connect_event(menu, widgetUtils.MENU, self.retweet, menuitem=menu.retweet) + widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.retweet) widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav) widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav) widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl) @@ -580,7 +402,7 @@ class baseBufferController(bufferController): self.session.settings.write() @_tweets_exist - def direct_message(self, *args, **kwargs): + def send_message(self, *args, **kwargs): tweet = self.get_right_tweet() if self.type == "dm": screen_name = self.session.get_user(tweet["message_create"]["sender_id"])["screen_name"] @@ -618,7 +440,7 @@ class baseBufferController(bufferController): if hasattr(dm.message, "destroy"): dm.message.destroy() @_tweets_exist - def retweet(self, *args, **kwargs): + def share_item(self, *args, **kwargs): tweet = self.get_right_tweet() id = tweet["id"] if self.session.settings["general"]["retweet_mode"] == "ask": @@ -912,58 +734,6 @@ class listBufferController(baseBufferController): elif dlg == widgetUtils.NO: return False -class eventsBufferController(bufferController): - def __init__(self, parent, name, session, account, *args, **kwargs): - super(eventsBufferController, self).__init__(parent, *args, **kwargs) - log.debug("Initializing buffer %s, account %s" % (name, account,)) - self.invisible = True - self.buffer = buffers.eventsPanel(parent, name) - self.name = name - self.account = account - self.buffer.account = self.account - self.compose_function = compose.compose_event - self.session = session - self.type = self.buffer.type - self.get_formatted_message = self.get_message - - def get_message(self): - if self.buffer.list.get_count() == 0: return _(u"Empty") - # fix this: - return "%s. %s" % (self.buffer.list.list.GetItemText(self.buffer.list.get_selected()), self.buffer.list.list.GetItemText(self.buffer.list.get_selected(), 1)) - - def add_new_item(self, item): - tweet = self.compose_function(item, self.session.db["user_name"], self.session.settings["general"]["show_screen_names"]) - if self.session.settings["general"]["reverse_timelines"] == False: - self.buffer.list.insert_item(False, *tweet) - else: - self.buffer.list.insert_item(True, *tweet) - if self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: - output.speak(" ".join(tweet), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"]) - if self.buffer.list.get_count() == 1: - self.buffer.list.select_item(0) - - def clear_list(self): - dlg = commonMessageDialogs.clear_list() - if dlg == widgetUtils.YES: - self.buffer.list.clear() - - def show_menu(self, ev, pos=0, *args, **kwargs): - if self.buffer.list.get_count() == 0: return - menu = menus.eventsPanelMenu() - widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view) - widgetUtils.connect_event(menu, widgetUtils.MENU, self.copy, menuitem=menu.copy) - widgetUtils.connect_event(menu, widgetUtils.MENU, self.destroy_status, menuitem=menu.remove) - if pos != 0: - self.buffer.PopupMenu(menu, pos) - else: - self.buffer.PopupMenu(menu, ev.GetPosition()) - - def view(self, *args, **kwargs): - pub.sendMessage("execute-action", action="view_item") - - def copy(self, *args, **kwargs): - pub.sendMessage("execute-action", action="copy_to_clipboard") - class peopleBufferController(baseBufferController): def __init__(self, parent, function, name, sessionObject, account, bufferType=None, *args, **kwargs): super(peopleBufferController, self).__init__(parent, function, name, sessionObject, account, bufferType="peoplePanel", *args, **kwargs) @@ -1129,7 +899,7 @@ class peopleBufferController(baseBufferController): def show_menu(self, ev, pos=0, *args, **kwargs): menu = menus.peoplePanelMenu() - widgetUtils.connect_event(menu, widgetUtils.MENU, self.direct_message, menuitem=menu.reply) + widgetUtils.connect_event(menu, widgetUtils.MENU, self.send_message, menuitem=menu.reply) widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions) widgetUtils.connect_event(menu, widgetUtils.MENU, self.details, menuitem=menu.details) # widgetUtils.connect_event(menu, widgetUtils.MENU, self.lists, menuitem=menu.lists) @@ -1289,7 +1059,7 @@ class searchPeopleBufferController(peopleBufferController): elif dlg == widgetUtils.NO: return False -class trendsBufferController(bufferController): +class trendsBufferController(baseBuffers.buffer): def __init__(self, parent, name, session, account, trendsFor, *args, **kwargs): super(trendsBufferController, self).__init__(parent=parent, session=session) self.trendsFor = trendsFor @@ -1339,7 +1109,7 @@ class trendsBufferController(bufferController): log.debug("Binding events...") self.buffer.list.list.Bind(wx.EVT_CHAR_HOOK, self.get_event) widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.tweet_about_this_trend, self.buffer.tweetTrendBtn) - widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_tweet, self.buffer.tweet) + widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.tweet) widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu) widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key) widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.search_topic, self.buffer.search_topic) diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 369cd4a7..12f993c6 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -22,7 +22,7 @@ elif system == "Linux": from sessions.twitter import utils, compose from sessionmanager import manager, sessionManager -import buffersController +from controller.buffers import baseBuffers, twitterBuffers import messages import sessions from sessions.twitter import session as session_ @@ -57,13 +57,13 @@ class Controller(object): name_ str: The name for the buffer user str: The account for the buffer. for example you may want to search the home_timeline buffer for the tw_blue2 user. - Return type: buffersController.buffer object.""" + Return type: buffers.buffer object.""" for i in self.buffers: if i.name == name_ and i.account == user: return i def get_current_buffer(self): """ Get the current focused bufferObject. - Return type: BuffersController.buffer object.""" + Return type: buffers.buffer object.""" buffer = self.view.get_current_buffer() if hasattr(buffer, "account"): buffer = self.search_buffer(buffer.name, buffer.account) @@ -72,7 +72,7 @@ class Controller(object): def get_best_buffer(self): """ Get the best buffer for doing something using the session object. This function is useful when you need to open a timeline or post a tweet, and the user is in a buffer without a session, for example the events buffer. - Return type: buffersController.buffer object.""" + Return type: twitterBuffers.buffer object.""" # Gets the parent buffer to know what account is doing an action view_buffer = self.view.get_current_buffer() # If the account has no session attached, we will need to search the first available non-empty buffer for that account to use its session. @@ -273,7 +273,7 @@ class Controller(object): def create_ignored_session_buffer(self, session): self.accounts.append(session.settings["twitter"]["user_name"]) - account = buffersController.accountPanel(self.view.nb, session.settings["twitter"]["user_name"], session.settings["twitter"]["user_name"], session.session_id) + account = baseBuffers.accountPanel(self.view.nb, session.settings["twitter"]["user_name"], session.settings["twitter"]["user_name"], session.session_id) account.logged = False account.setup_account() self.buffers.append(account) @@ -293,96 +293,96 @@ class Controller(object): session.get_user_info() if createAccounts == True: self.accounts.append(session.db["user_name"]) - account = buffersController.accountPanel(self.view.nb, session.db["user_name"], session.db["user_name"], session.session_id) + account = baseBuffers.accountPanel(self.view.nb, session.db["user_name"], session.db["user_name"], session.session_id) account.setup_account() self.buffers.append(account) self.view.add_buffer(account.buffer , name=session.db["user_name"]) for i in session.settings['general']['buffer_order']: if i == 'home': - home = buffersController.baseBufferController(self.view.nb, "get_home_timeline", "home_timeline", session, session.db["user_name"], sound="tweet_received.ogg", tweet_mode="extended") + home = twitterBuffers.baseBufferController(self.view.nb, "get_home_timeline", "home_timeline", session, session.db["user_name"], sound="tweet_received.ogg", tweet_mode="extended") self.buffers.append(home) self.view.insert_buffer(home.buffer, name=_(u"Home"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'mentions': - mentions = buffersController.baseBufferController(self.view.nb, "get_mentions_timeline", "mentions", session, session.db["user_name"], sound="mention_received.ogg", tweet_mode="extended") + mentions = twitterBuffers.baseBufferController(self.view.nb, "get_mentions_timeline", "mentions", session, session.db["user_name"], sound="mention_received.ogg", tweet_mode="extended") self.buffers.append(mentions) self.view.insert_buffer(mentions.buffer, name=_(u"Mentions"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'dm': - dm = buffersController.directMessagesController(self.view.nb, "get_direct_messages", "direct_messages", session, session.db["user_name"], bufferType="dmPanel", compose_func="compose_direct_message", sound="dm_received.ogg", full_text=True, items="events") + dm = twitterBuffers.directMessagesController(self.view.nb, "get_direct_messages", "direct_messages", session, session.db["user_name"], bufferType="dmPanel", compose_func="compose_direct_message", sound="dm_received.ogg", full_text=True, items="events") self.buffers.append(dm) self.view.insert_buffer(dm.buffer, name=_(u"Direct messages"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'sent_dm': - sent_dm = buffersController.sentDirectMessagesController(self.view.nb, "", "sent_direct_messages", session, session.db["user_name"], bufferType="dmPanel", compose_func="compose_direct_message") + sent_dm = twitterBuffers.sentDirectMessagesController(self.view.nb, "", "sent_direct_messages", session, session.db["user_name"], bufferType="dmPanel", compose_func="compose_direct_message") self.buffers.append(sent_dm) self.view.insert_buffer(sent_dm.buffer, name=_(u"Sent direct messages"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'sent_tweets': - sent_tweets = buffersController.baseBufferController(self.view.nb, "get_user_timeline", "sent_tweets", session, session.db["user_name"], screen_name=session.db["user_name"], tweet_mode="extended") + sent_tweets = twitterBuffers.baseBufferController(self.view.nb, "get_user_timeline", "sent_tweets", session, session.db["user_name"], screen_name=session.db["user_name"], tweet_mode="extended") self.buffers.append(sent_tweets) self.view.insert_buffer(sent_tweets.buffer, name=_(u"Sent tweets"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'favorites': - favourites = buffersController.baseBufferController(self.view.nb, "get_favorites", "favourites", session, session.db["user_name"], sound="favourite.ogg", tweet_mode="extended") + favourites = twitterBuffers.baseBufferController(self.view.nb, "get_favorites", "favourites", session, session.db["user_name"], sound="favourite.ogg", tweet_mode="extended") self.buffers.append(favourites) self.view.insert_buffer(favourites.buffer, name=_(u"Likes"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'followers': - followers = buffersController.peopleBufferController(self.view.nb, "get_followers_list", "followers", session, session.db["user_name"], sound="update_followers.ogg", screen_name=session.db["user_name"]) + followers = twitterBuffers.peopleBufferController(self.view.nb, "get_followers_list", "followers", session, session.db["user_name"], sound="update_followers.ogg", screen_name=session.db["user_name"]) self.buffers.append(followers) self.view.insert_buffer(followers.buffer, name=_(u"Followers"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'friends': - friends = buffersController.peopleBufferController(self.view.nb, "get_friends_list", "friends", session, session.db["user_name"], screen_name=session.db["user_name"]) + friends = twitterBuffers.peopleBufferController(self.view.nb, "get_friends_list", "friends", session, session.db["user_name"], screen_name=session.db["user_name"]) self.buffers.append(friends) self.view.insert_buffer(friends.buffer, name=_(u"Friends"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'blocks': - blocks = buffersController.peopleBufferController(self.view.nb, "list_blocks", "blocked", session, session.db["user_name"]) + blocks = twitterBuffers.peopleBufferController(self.view.nb, "list_blocks", "blocked", session, session.db["user_name"]) self.buffers.append(blocks) self.view.insert_buffer(blocks.buffer, name=_(u"Blocked users"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) elif i == 'muted': - muted = buffersController.peopleBufferController(self.view.nb, "list_mutes", "muted", session, session.db["user_name"]) + muted = twitterBuffers.peopleBufferController(self.view.nb, "list_mutes", "muted", session, session.db["user_name"]) self.buffers.append(muted) self.view.insert_buffer(muted.buffer, name=_(u"Muted users"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) - timelines = buffersController.emptyPanel(self.view.nb, "timelines", session.db["user_name"]) + timelines = baseBuffers.emptyPanel(self.view.nb, "timelines", session.db["user_name"]) self.buffers.append(timelines) self.view.insert_buffer(timelines.buffer , name=_(u"Timelines"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) for i in session.settings["other_buffers"]["timelines"]: - tl = buffersController.baseBufferController(self.view.nb, "get_user_timeline", "%s-timeline" % (i,), session, session.db["user_name"], sound="tweet_timeline.ogg", bufferType=None, user_id=i, tweet_mode="extended") + tl = twitterBuffers.baseBufferController(self.view.nb, "get_user_timeline", "%s-timeline" % (i,), session, session.db["user_name"], sound="tweet_timeline.ogg", bufferType=None, user_id=i, tweet_mode="extended") self.buffers.append(tl) self.view.insert_buffer(tl.buffer, name=_(u"Timeline for {}").format(i,), pos=self.view.search("timelines", session.db["user_name"])) - favs_timelines = buffersController.emptyPanel(self.view.nb, "favs_timelines", session.db["user_name"]) + favs_timelines = baseBuffers.emptyPanel(self.view.nb, "favs_timelines", session.db["user_name"]) self.buffers.append(favs_timelines) self.view.insert_buffer(favs_timelines.buffer , name=_(u"Likes timelines"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) for i in session.settings["other_buffers"]["favourites_timelines"]: - tl = buffersController.baseBufferController(self.view.nb, "get_favorites", "%s-favorite" % (i,), session, session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=i, tweet_mode="extended") + tl = twitterBuffers.baseBufferController(self.view.nb, "get_favorites", "%s-favorite" % (i,), session, session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=i, tweet_mode="extended") self.buffers.append(tl) self.view.insert_buffer(tl.buffer, name=_(u"Likes for {}").format(i,), pos=self.view.search("favs_timelines", session.db["user_name"])) - followers_timelines = buffersController.emptyPanel(self.view.nb, "followers_timelines", session.db["user_name"]) + followers_timelines = baseBuffers.emptyPanel(self.view.nb, "followers_timelines", session.db["user_name"]) self.buffers.append(followers_timelines) self.view.insert_buffer(followers_timelines.buffer , name=_(u"Followers' Timelines"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) for i in session.settings["other_buffers"]["followers_timelines"]: - tl = buffersController.peopleBufferController(self.view.nb, "get_followers_list", "%s-followers" % (i,), session, session.db["user_name"], sound="new_event.ogg", user_id=i) + tl = twitterBuffers.peopleBufferController(self.view.nb, "get_followers_list", "%s-followers" % (i,), session, session.db["user_name"], sound="new_event.ogg", user_id=i) self.buffers.append(tl) self.view.insert_buffer(tl.buffer, name=_(u"Followers for {}").format(i,), pos=self.view.search("followers_timelines", session.db["user_name"])) - friends_timelines = buffersController.emptyPanel(self.view.nb, "friends_timelines", session.db["user_name"]) + friends_timelines = baseBuffers.emptyPanel(self.view.nb, "friends_timelines", session.db["user_name"]) self.buffers.append(friends_timelines) self.view.insert_buffer(friends_timelines.buffer , name=_(u"Friends' Timelines"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) for i in session.settings["other_buffers"]["friends_timelines"]: - tl = buffersController.peopleBufferController(self.view.nb, "get_friends_list", "%s-friends" % (i,), session, session.db["user_name"], sound="new_event.ogg", user_id=i) + tl = twitterBuffers.peopleBufferController(self.view.nb, "get_friends_list", "%s-friends" % (i,), session, session.db["user_name"], sound="new_event.ogg", user_id=i) self.buffers.append(tl) self.view.insert_buffer(tl.buffer, name=_(u"Friends for {}").format(i,), pos=self.view.search("friends_timelines", session.db["user_name"])) - lists = buffersController.emptyPanel(self.view.nb, "lists", session.db["user_name"]) + lists = baseBuffers.emptyPanel(self.view.nb, "lists", session.db["user_name"]) self.buffers.append(lists) self.view.insert_buffer(lists.buffer , name=_(u"Lists"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) for i in session.settings["other_buffers"]["lists"]: - tl = buffersController.listBufferController(self.view.nb, "get_list_statuses", "%s-list" % (i,), session, session.db["user_name"], bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), tweet_mode="extended") + tl = twitterBuffers.listBufferController(self.view.nb, "get_list_statuses", "%s-list" % (i,), session, session.db["user_name"], bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), tweet_mode="extended") session.lists.append(tl) self.buffers.append(tl) self.view.insert_buffer(tl.buffer, name=_(u"List for {}").format(i), pos=self.view.search("lists", session.db["user_name"])) - searches = buffersController.emptyPanel(self.view.nb, "searches", session.db["user_name"]) + searches = baseBuffers.emptyPanel(self.view.nb, "searches", session.db["user_name"]) self.buffers.append(searches) self.view.insert_buffer(searches.buffer , name=_(u"Searches"), pos=self.view.search(session.db["user_name"], session.db["user_name"])) for i in session.settings["other_buffers"]["tweet_searches"]: - tl = buffersController.searchBufferController(self.view.nb, "search", "%s-searchterm" % (i,), session, session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=i, tweet_mode="extended") + tl = twitterBuffers.searchBufferController(self.view.nb, "search", "%s-searchterm" % (i,), session, session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=i, tweet_mode="extended") self.buffers.append(tl) self.view.insert_buffer(tl.buffer, name=_(u"Search for {}").format(i), pos=self.view.search("searches", session.db["user_name"])) for i in session.settings["other_buffers"]["trending_topic_buffers"]: - buffer = buffersController.trendsBufferController(self.view.nb, "%s_tt" % (i,), session, session.db["user_name"], i, sound="trends_updated.ogg") + buffer = twitterBuffers.trendsBufferController(self.view.nb, "%s_tt" % (i,), session, session.db["user_name"], i, sound="trends_updated.ogg") buffer.start_stream(play_sound=False) buffer.searchfunction = self.search self.buffers.append(buffer) @@ -430,12 +430,12 @@ class Controller(object): buffer.session.settings["other_buffers"]["tweet_searches"].append(term) buffer.session.settings.write() args = {"lang": dlg.get_language(), "result_type": dlg.get_result_type()} - search = buffersController.searchBufferController(self.view.nb, "search", "%s-searchterm" % (term,), buffer.session, buffer.session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=term, tweet_mode="extended", **args) + search = twitterBuffers.searchBufferController(self.view.nb, "search", "%s-searchterm" % (term,), buffer.session, buffer.session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=term, tweet_mode="extended", **args) else: log.error("A buffer for the %s search term is already created. You can't create a duplicate buffer." % (term,)) return elif dlg.get("users") == True: - search = buffersController.searchPeopleBufferController(self.view.nb, "search_users", "%s-searchUser" % (term,), buffer.session, buffer.session.db["user_name"], bufferType=None, sound="search_updated.ogg", q=term) + search = twitterBuffers.searchPeopleBufferController(self.view.nb, "search_users", "%s-searchUser" % (term,), buffer.session, buffer.session.db["user_name"], bufferType=None, sound="search_updated.ogg", q=term) search.start_stream(mandatory=True) pos=self.view.search("searches", buffer.session.db["user_name"]) self.insert_buffer(search, pos) @@ -742,25 +742,25 @@ class Controller(object): def post_tweet(self, event=None): buffer = self.get_best_buffer() - buffer.post_tweet() + buffer.post_status() def post_reply(self, *args, **kwargs): buffer = self.get_current_buffer() if buffer.name == "direct_messages": - buffer.direct_message() + buffer.send_message() else: buffer.reply() def send_dm(self, *args, **kwargs): buffer = self.get_current_buffer() - buffer.direct_message() + buffer.send_message() def post_retweet(self, *args, **kwargs): buffer = self.get_current_buffer() if buffer.type == "dm" or buffer.type == "people" or buffer.type == "events": return else: - buffer.retweet() + buffer.share_item() def add_to_favourites(self, *args, **kwargs): buffer = self.get_current_buffer() @@ -836,7 +836,7 @@ class Controller(object): if usr["id_str"] in buff.session.settings["other_buffers"]["timelines"]: commonMessageDialogs.timeline_exist() return - tl = buffersController.baseBufferController(self.view.nb, "get_user_timeline", "%s-timeline" % (usr["id_str"],), buff.session, buff.session.db["user_name"], bufferType=None, sound="tweet_timeline.ogg", user_id=usr["id_str"], tweet_mode="extended") + tl = twitterBuffers.baseBufferController(self.view.nb, "get_user_timeline", "%s-timeline" % (usr["id_str"],), buff.session, buff.session.db["user_name"], bufferType=None, sound="tweet_timeline.ogg", user_id=usr["id_str"], tweet_mode="extended") try: tl.start_stream(play_sound=False) except TwythonAuthError: @@ -855,7 +855,7 @@ class Controller(object): if usr["id_str"] in buff.session.settings["other_buffers"]["favourites_timelines"]: commonMessageDialogs.timeline_exist() return - tl = buffersController.baseBufferController(self.view.nb, "get_favorites", "%s-favorite" % (usr["id_str"],), buff.session, buff.session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=usr["id_str"], tweet_mode="extended") + tl = twitterBuffers.baseBufferController(self.view.nb, "get_favorites", "%s-favorite" % (usr["id_str"],), buff.session, buff.session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=usr["id_str"], tweet_mode="extended") try: tl.start_stream(play_sound=False) except TwythonAuthError: @@ -874,7 +874,7 @@ class Controller(object): if usr["id_str"] in buff.session.settings["other_buffers"]["followers_timelines"]: commonMessageDialogs.timeline_exist() return - tl = buffersController.peopleBufferController(self.view.nb, "get_followers_list", "%s-followers" % (usr["id_str"],), buff.session, buff.session.db["user_name"], sound="new_event.ogg", user_id=usr["id_str"]) + tl = twitterBuffers.peopleBufferController(self.view.nb, "get_followers_list", "%s-followers" % (usr["id_str"],), buff.session, buff.session.db["user_name"], sound="new_event.ogg", user_id=usr["id_str"]) try: tl.start_stream(play_sound=False) except TwythonAuthError: @@ -893,7 +893,7 @@ class Controller(object): if usr["id_str"] in buff.session.settings["other_buffers"]["friends_timelines"]: commonMessageDialogs.timeline_exist() return - tl = buffersController.peopleBufferController(self.view.nb, "get_friends_list", "%s-friends" % (usr["id_str"],), buff.session, buff.session.db["user_name"], sound="new_event.ogg", user_id=usr["id_str"]) + tl = twitterBuffers.peopleBufferController(self.view.nb, "get_friends_list", "%s-friends" % (usr["id_str"],), buff.session, buff.session.db["user_name"], sound="new_event.ogg", user_id=usr["id_str"]) try: tl.start_stream(play_sound=False) except TwythonAuthError: @@ -913,7 +913,7 @@ class Controller(object): buffer = self.get_current_buffer() id = buffer.get_right_tweet()["id_str"] user = buffer.get_right_tweet()["user"]["screen_name"] - search = buffersController.conversationBufferController(self.view.nb, "search", "%s-searchterm" % (id,), buffer.session, buffer.session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", since_id=id, q="@{0}".format(user,)) + search = twitterBuffers.conversationBufferController(self.view.nb, "search", "%s-searchterm" % (id,), buffer.session, buffer.session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", since_id=id, q="@{0}".format(user,)) search.tweet = buffer.get_right_tweet() search.start_stream(start=True) pos=self.view.search("searches", buffer.session.db["user_name"]) @@ -940,7 +940,7 @@ class Controller(object): if trends.dialog.get_response() == widgetUtils.OK: woeid = trends.get_woeid() if woeid in buff.session.settings["other_buffers"]["trending_topic_buffers"]: return - buffer = buffersController.trendsBufferController(self.view.nb, "%s_tt" % (woeid,), buff.session, buff.account, woeid, sound="trends_updated.ogg") + buffer = twitterBuffers.trendsBufferController(self.view.nb, "%s_tt" % (woeid,), buff.session, buff.account, woeid, sound="trends_updated.ogg") buffer.searchfunction = self.search pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"]) self.view.insert_buffer(buffer.buffer, name=_(u"Trending topics for %s") % (trends.get_string()), pos=pos) @@ -1341,32 +1341,32 @@ class Controller(object): buff = self.search_buffer("home_timeline", account) if create == True: if buffer == "favourites": - favourites = buffersController.baseBufferController(self.view.nb, "get_favorites", "favourites", buff.session, buff.session.db["user_name"], tweet_mode="extended") + favourites = twitterBuffers.baseBufferController(self.view.nb, "get_favorites", "favourites", buff.session, buff.session.db["user_name"], tweet_mode="extended") self.buffers.append(favourites) self.view.insert_buffer(favourites.buffer, name=_(u"Likes"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) favourites.start_stream(play_sound=False) if buffer == "followers": - followers = buffersController.peopleBufferController(self.view.nb, "get_followers_list", "followers", buff.session, buff.session.db["user_name"], screen_name=buff.session.db["user_name"]) + followers = twitterBuffers.peopleBufferController(self.view.nb, "get_followers_list", "followers", buff.session, buff.session.db["user_name"], screen_name=buff.session.db["user_name"]) self.buffers.append(followers) self.view.insert_buffer(followers.buffer, name=_(u"Followers"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) followers.start_stream(play_sound=False) elif buffer == "friends": - friends = buffersController.peopleBufferController(self.view.nb, "get_friends_list", "friends", buff.session, buff.session.db["user_name"], screen_name=buff.session.db["user_name"]) + friends = twitterBuffers.peopleBufferController(self.view.nb, "get_friends_list", "friends", buff.session, buff.session.db["user_name"], screen_name=buff.session.db["user_name"]) self.buffers.append(friends) self.view.insert_buffer(friends.buffer, name=_(u"Friends"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) friends.start_stream(play_sound=False) elif buffer == "blocked": - blocks = buffersController.peopleBufferController(self.view.nb, "list_blocks", "blocked", buff.session, buff.session.db["user_name"]) + blocks = twitterBuffers.peopleBufferController(self.view.nb, "list_blocks", "blocked", buff.session, buff.session.db["user_name"]) self.buffers.append(blocks) self.view.insert_buffer(blocks.buffer, name=_(u"Blocked users"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) blocks.start_stream(play_sound=False) elif buffer == "muted": - muted = buffersController.peopleBufferController(self.view.nb, "get_muted_users_list", "muted", buff.session, buff.session.db["user_name"]) + muted = twitterBuffers.peopleBufferController(self.view.nb, "get_muted_users_list", "muted", buff.session, buff.session.db["user_name"]) self.buffers.append(muted) self.view.insert_buffer(muted.buffer, name=_(u"Muted users"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) muted.start_stream(play_sound=False) elif buffer == "events": - events = buffersController.eventsBufferController(self.view.nb, "events", buff.session, buff.session.db["user_name"], bufferType="dmPanel", screen_name=buff.session.db["user_name"]) + events = twitterBuffers.eventsBufferController(self.view.nb, "events", buff.session, buff.session.db["user_name"], bufferType="dmPanel", screen_name=buff.session.db["user_name"]) self.buffers.append(events) self.view.insert_buffer(events.buffer, name=_(u"Events"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) elif create == False: @@ -1375,7 +1375,7 @@ class Controller(object): if create in buff.session.settings["other_buffers"]["lists"]: output.speak(_(u"This list is already opened"), True) return - tl = buffersController.listBufferController(self.view.nb, "get_list_statuses", create+"-list", buff.session, buff.session.db["user_name"], bufferType=None, list_id=utils.find_list(create, buff.session.db["lists"]), tweet_mode="extended") + tl = twitterBuffers.listBufferController(self.view.nb, "get_list_statuses", create+"-list", buff.session, buff.session.db["user_name"], bufferType=None, list_id=utils.find_list(create, buff.session.db["lists"]), tweet_mode="extended") buff.session.lists.append(tl) pos=self.view.search("lists", buff.session.db["user_name"]) self.insert_buffer(tl, pos) From 40105f37ed07c6729d68a3f655ec88306adfe456 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 29 Sep 2018 11:13:11 -0500 Subject: [PATCH 55/75] Added support for deleting dm's with the new Twitter API methods --- doc/changelog.md | 1 + src/controller/buffers/twitterBuffers.py | 1 - src/twython/api.py | 8 ++++++-- src/twython/endpoints.py | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 41a0682f..950b403d 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* Added support for deleting direct messages by using the new Twitter API methods. * When quoting a retweet, the quote will be made to the original tweet instead of the retweet. * If the sent direct messages buffer is hidden, TWBlue should keep loading everything as expected. ([#246](https://github.com/manuelcortez/TWBlue/issues/246)) * There is a new soundpack, called FreakyBlue (Thanks to [Andre Louis](https://twitter.com/FreakyFwoof)) as a new option in TWBlue. This pack can be the default in the next stable, so users can take a look and share their opinion in snapshot versions. ([#247](https://github.com/manuelcortez/TWBlue/issues/247)) diff --git a/src/controller/buffers/twitterBuffers.py b/src/controller/buffers/twitterBuffers.py index 225de733..1ed2a34a 100644 --- a/src/controller/buffers/twitterBuffers.py +++ b/src/controller/buffers/twitterBuffers.py @@ -557,7 +557,6 @@ class baseBufferController(baseBuffers.buffer): self.session.twitter.destroy_status(id=self.get_right_tweet()["id"]) self.session.db[self.name].pop(index) self.buffer.list.remove_item(index) -# if index > 0: except TwythonError: self.session.sound.play("error.ogg") diff --git a/src/twython/api.py b/src/twython/api.py index 10c4963c..96d1ae72 100644 --- a/src/twython/api.py +++ b/src/twython/api.py @@ -153,7 +153,7 @@ class Twython(EndpointsMixin, object): if k in ('timeout', 'allow_redirects', 'stream', 'verify'): requests_args[k] = v - if method == 'get': + if method == 'get' or method == 'delete': requests_args['params'] = params else: # Check for json_encoded so we will sent params as "data" or "json" @@ -242,7 +242,7 @@ class Twython(EndpointsMixin, object): (e.g. search/tweets) :type endpoint: string :param method: (optional) Method of accessing data, either - GET or POST. (default GET) + GET, POST or DELETE. (default GET) :type method: string :param params: (optional) Dict of parameters (if any) accepted the by Twitter API endpoint you are trying to @@ -281,6 +281,10 @@ class Twython(EndpointsMixin, object): """Shortcut for POST requests via :class:`request`""" return self.request(endpoint, 'POST', params=params, version=version, json_encoded=json_encoded) + def delete(self, endpoint, params=None, version='1.1', json_encoded=False): + """Shortcut for delete requests via :class:`request`""" + return self.request(endpoint, 'DELETE', params=params, version=version, json_encoded=json_encoded) + def get_lastfunction_header(self, header, default_return_value=None): """Returns a specific header from the last API call This will return None if the header is not present diff --git a/src/twython/endpoints.py b/src/twython/endpoints.py index f7e0a5a6..9adf800f 100644 --- a/src/twython/endpoints.py +++ b/src/twython/endpoints.py @@ -329,10 +329,10 @@ class EndpointsMixin(object): """Destroys the direct message specified in the required ``id`` parameter Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message-event """ - return self.post('direct_messages/destroy', params=params) + return self.delete('direct_messages/events/destroy', params=params) def send_direct_message(self, **params): """Sends a new direct message to the specified user from the From 74e020c0906ac8930431730cefef0c686055cb7c Mon Sep 17 00:00:00 2001 From: Jose Manuel Delicado Date: Sun, 30 Sep 2018 11:56:47 +0200 Subject: [PATCH 56/75] Added certifi and backports.functools-lru-cache to requirements.txt --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 65691da8..38d64f93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,6 @@ chardet urllib3 youtube-dl python-vlc -pywin32 \ No newline at end of file +pywin32 +certifi +backports.functools_lru_cache From c85c478595a5d9a1f359721770940f898c0f685d Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 30 Sep 2018 14:39:59 -0500 Subject: [PATCH 57/75] Added date and time wen displaying tweets and dm's --- doc/changelog.md | 1 + src/controller/mainController.py | 16 ++++++++++------ src/controller/messages.py | 10 +++++++--- src/wxUI/dialogs/message.py | 25 +++++++++++++++++++++++-- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 950b403d..238a08dc 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* When displaying tweets or direct messages, a new field has been added to show the date when the item has been posted to Twitter. * Added support for deleting direct messages by using the new Twitter API methods. * When quoting a retweet, the quote will be made to the original tweet instead of the retweet. * If the sent direct messages buffer is hidden, TWBlue should keep loading everything as expected. ([#246](https://github.com/manuelcortez/TWBlue/issues/246)) diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 12f993c6..17e20ae6 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -4,6 +4,7 @@ system = platform.system() import application import requests import youtube_utils +import arrow if system == "Windows": from update import updater from wxUI import (view, dialogs, commonMessageDialogs, sysTrayIcon) @@ -792,14 +793,17 @@ class Controller(object): def view_item(self, *args, **kwargs): buffer = self.get_current_buffer() - if buffer.type == "baseBuffer" or buffer.type == "favourites_timeline" or buffer.type == "list" or buffer.type == "search": - tweet, tweetsList = buffer.get_full_tweet() - msg = messages.viewTweet(tweet, tweetsList) - elif buffer.type == "account" or buffer.type == "empty": + if buffer.type == "account" or buffer.type == "empty": return - elif buffer.name == "sent_tweets": + elif buffer.type == "baseBuffer" or buffer.type == "favourites_timeline" or buffer.type == "list" or buffer.type == "search": tweet, tweetsList = buffer.get_full_tweet() - msg = messages.viewTweet(tweet, tweetsList) + msg = messages.viewTweet(tweet, tweetsList, utc_offset=buffer.session.db["utc_offset"]) + elif buffer.type == "dm": + non_tweet = buffer.get_formatted_message() + item = buffer.get_right_tweet() + original_date = arrow.get(item["created_timestamp"][:-3]) + date = original_date.replace(seconds=buffer.session.db["utc_offset"]).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage()) + msg = messages.viewTweet(non_tweet, [], False, date=date) else: non_tweet = buffer.get_formatted_message() msg = messages.viewTweet(non_tweet, [], False) diff --git a/src/controller/messages.py b/src/controller/messages.py index 5cda8c0a..d423764f 100644 --- a/src/controller/messages.py +++ b/src/controller/messages.py @@ -2,6 +2,8 @@ import re import platform import attach +import arrow +import languageHandler system = platform.system() import widgetUtils import output @@ -193,7 +195,7 @@ class dm(basicTweet): c.show_menu("dm") class viewTweet(basicTweet): - def __init__(self, tweet, tweetList, is_tweet=True): + def __init__(self, tweet, tweetList, is_tweet=True, utc_offset=0, date=""): """ This represents a tweet displayer. However it could be used for showing something wich is not a tweet, like a direct message or an event. param tweet: A dictionary that represents a full tweet or a string for non-tweets. param tweetList: If is_tweet is set to True, this could be a list of quoted tweets. @@ -229,6 +231,8 @@ class viewTweet(basicTweet): favs_count = str(tweet["favorite_count"]) # Gets the client from where this tweet was made. source = str(re.sub(r"(?s)<.*?>", "", tweet["source"].encode("utf-8"))) + original_date = arrow.get(tweet["created_at"], "ddd MMM DD H:m:s Z YYYY", locale="en") + date = original_date.replace(seconds=utc_offset).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage()) if text == "": if tweet.has_key("message"): value = "message" @@ -250,13 +254,13 @@ class viewTweet(basicTweet): for z in tweet["retweeted_status"]["extended_entities"]["media"]: if z.has_key("ext_alt_text") and z["ext_alt_text"] != None: image_description.append(z["ext_alt_text"]) - self.message = message.viewTweet(text, rt_count, favs_count, source.decode("utf-8")) + self.message = message.viewTweet(text, rt_count, favs_count, source.decode("utf-8"), date) self.message.set_title(len(text)) [self.message.set_image_description(i) for i in image_description] else: self.title = _(u"View item") text = tweet - self.message = message.viewNonTweet(text) + self.message = message.viewNonTweet(text, date) widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck) widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate) if self.contain_urls() == True: diff --git a/src/wxUI/dialogs/message.py b/src/wxUI/dialogs/message.py index d570344b..c22cebfc 100644 --- a/src/wxUI/dialogs/message.py +++ b/src/wxUI/dialogs/message.py @@ -298,7 +298,7 @@ class viewTweet(widgetUtils.BaseDialog): def set_title(self, lenght): self.SetTitle(_(u"Tweet - %i characters ") % (lenght,)) - def __init__(self, text, rt_count, favs_count,source, *args, **kwargs): + def __init__(self, text, rt_count, favs_count, source, date="", *args, **kwargs): super(viewTweet, self).__init__(None, size=(850,850)) panel = wx.Panel(self) label = wx.StaticText(panel, -1, _(u"Tweet")) @@ -339,11 +339,21 @@ class viewTweet(widgetUtils.BaseDialog): sourceBox = wx.BoxSizer(wx.HORIZONTAL) sourceBox.Add(sourceLabel, 0, wx.ALL, 5) sourceBox.Add(sourceTweet, 0, wx.ALL, 5) + dateLabel = wx.StaticText(panel, -1, _(u"Date: ")) + dateTweet = wx.TextCtrl(panel, -1, date, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) + dc = wx.WindowDC(dateTweet) + dc.SetFont(dateTweet.GetFont()) + (x, y) = dc.GetTextExtent("0"*100) + dateTweet.SetSize((x, y)) + dateBox = wx.BoxSizer(wx.HORIZONTAL) + dateBox.Add(dateLabel, 0, wx.ALL, 5) + dateBox.Add(dateTweet, 0, wx.ALL, 5) infoBox = wx.BoxSizer(wx.HORIZONTAL) infoBox.Add(rtBox, 0, wx.ALL, 5) infoBox.Add(favsBox, 0, wx.ALL, 5) infoBox.Add(sourceBox, 0, wx.ALL, 5) mainBox.Add(infoBox, 0, wx.ALL, 5) + mainBox.Add(dateBox, 0, wx.ALL, 5) self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize) self.unshortenButton = wx.Button(panel, -1, _(u"&Expand URL"), size=wx.DefaultSize) self.unshortenButton.Disable() @@ -390,7 +400,7 @@ class viewTweet(widgetUtils.BaseDialog): class viewNonTweet(widgetUtils.BaseDialog): - def __init__(self, text, *args, **kwargs): + def __init__(self, text, date="", *args, **kwargs): super(viewNonTweet, self).__init__(None, size=(850,850)) self.SetTitle(_(u"View")) panel = wx.Panel(self) @@ -406,6 +416,17 @@ class viewNonTweet(widgetUtils.BaseDialog): textBox.Add(self.text, 1, wx.EXPAND, 5) mainBox = wx.BoxSizer(wx.VERTICAL) mainBox.Add(textBox, 0, wx.ALL, 5) + if date != "": + dateLabel = wx.StaticText(panel, -1, _(u"Date: ")) + date = wx.TextCtrl(panel, -1, date, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE) + dc = wx.WindowDC(date) + dc.SetFont(date.GetFont()) + (x, y) = dc.GetTextExtent("0"*100) + date.SetSize((x, y)) + dateBox = wx.BoxSizer(wx.HORIZONTAL) + dateBox.Add(dateLabel, 0, wx.ALL, 5) + dateBox.Add(date, 0, wx.ALL, 5) + mainBox.Add(dateBox, 0, wx.ALL, 5) self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize) self.unshortenButton = wx.Button(panel, -1, _(u"&Expand URL"), size=wx.DefaultSize) self.unshortenButton.Disable() From b6fa1319997141da0c382eef9c72494925fc2d01 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 3 Oct 2018 09:03:15 -0500 Subject: [PATCH 58/75] Fixed typos in changelog --- doc/changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 238a08dc..4f3e6ede 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -24,14 +24,14 @@ * The new method does not allow direct messages to be processed in real time. Direct messages will be updated periodically. * After august 16 or when streaming is disabled, the events buffer will no longer be created in TWBlue. * You can configure frequency for buffer updates in TWBlue. By default, TWBlue will update all buffers every 2 minutes, but you can change this setting in the global settings dialog. ([#223](https://github.com/manuelcortez/TWBlue/issues/223)) -* Added a new tab called feedback, in the account settings dialog. This tab allows you to control wether automatic speech or Braille feedbak in certain events (mentions and direct messages received) is enabled. Take into account that this option will take preference over automatic reading of buffers and any kind of automatic output. ([#203](https://github.com/manuelcortez/TWBlue/issues/203)) +* Added a new tab called feedback, in the account settings dialog. This tab allows you to control whether automatic speech or Braille feedbak in certain events (mentions and direct messages received) is enabled. Take into account that this option will take preference over automatic reading of buffers and any kind of automatic output. ([#203](https://github.com/manuelcortez/TWBlue/issues/203)) * The spell checking dialog now has access keys defined for the most important actions. ([#211](https://github.com/manuelcortez/TWBlue/issues/211)) * TWBlue now Uses WXPython 4.0.1. This will allow us to migrate all important components to Python 3 in the future. ([#207](https://github.com/manuelcortez/TWBlue/issues/207)) * When you quote a Tweet, if the original tweet was posted with Twishort, TWBlue should display properly the quoted tweet. Before it was displaying the original tweet only. ([#206](https://github.com/manuelcortez/TWBlue/issues/206)) * It is possible to filter by retweets, quotes and replies when creating a new filter. * Added support for playing youtube Links directly from the client. ([#94](https://github.com/manuelcortez/TWBlue/issues/94)) * Replaced Bass with libVLC for playing URL streams. -* the checkbox for indicating wether TWBlue will include everyone in a reply or not, will be unchecked by default. +* the checkbox for indicating whether TWBlue will include everyone in a reply or not, will be unchecked by default. * You can request TWBlue to save the state for two checkboxes: Long tweet and mention all, from the global settings dialogue. * For windows 10 users, some keystrokes in the invisible user interface have been changed or merged: * control+Windows+alt+F will be used for toggling between adding and removing a tweet to user's likes. This function will execute the needed action based in the current status for the focused tweet. From 36ba6eca92aa00019f2ef2e1519c2c5e60f1f0d7 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Fri, 19 Oct 2018 17:35:56 -0500 Subject: [PATCH 59/75] Started refactoring of autoreading for buffers --- src/controller/buffers/twitterBuffers.py | 22 +++++++++++++++++++++- src/controller/mainController.py | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/controller/buffers/twitterBuffers.py b/src/controller/buffers/twitterBuffers.py index 1ed2a34a..36cb8cb9 100644 --- a/src/controller/buffers/twitterBuffers.py +++ b/src/controller/buffers/twitterBuffers.py @@ -63,6 +63,13 @@ class baseBufferController(baseBuffers.buffer): self.kwargs["screen_name"] = self.kwargs["user_id"] self.kwargs.pop("user_id") + def get_buffer_name(self): + """ Get buffer name from a set of different techniques.""" + # firstly let's take the easier buffers. + basic_buffers = dict(home_timeline=_(u"Home"), mentions=_(u"Mentions"), direct_messages=_(u"Direct messages"), sent_direct_messages=_(u"Sent direct messages"), sent_tweets=_(u"Sent tweets"), favourites=_(u"Likes"), followers=_(u"Followers"), friends=_(u"Friends"), blocked=_(u"Blocked users"), muted=_(u"Muted users")) + if self.name in basic_buffers.keys(): + return basic_buffers[self.name] + def post_status(self, *args, **kwargs): title = _(u"Tweet") caption = _(u"Write the tweet here") @@ -139,7 +146,7 @@ class baseBufferController(baseBuffers.buffer): tweetsList.append(tweet) return (tweet, tweetsList) - def start_stream(self, mandatory=False, play_sound=True): + def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False): # starts stream every 3 minutes. current_time = time.time() if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True: @@ -161,8 +168,21 @@ class baseBufferController(baseBuffers.buffer): self.finished_timeline = True if number_of_items > 0 and self.name != "sent_tweets" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True: self.session.sound.play(self.sound) + # Autoread settings + if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]: + self.auto_read(number_of_items) return number_of_items + def auto_read(self, number_of_items): + if number_of_items == 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: + if self.session.settings["general"]["reverse_timelines"] == False: + tweet = self.session.db[self.name][0] + else: + tweet = self.session.db[self.name][-1] + output.speak(" ".join(tweet[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"]) + elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: + output.speak(_(u"{0} tweets in {1}.").format(number_of_items, self.get_buffer_name())) + def get_more_items(self): elements = [] if self.session.settings["general"]["reverse_timelines"] == False: diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 17e20ae6..e9d9f802 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -1537,7 +1537,7 @@ class Controller(object): return else: output.speak(_(u"Updating buffer...")) - n = bf.start_stream(mandatory=True) + n = bf.start_stream(mandatory=True, avoid_autoreading=True) if n != None: output.speak(_(u"{0} items retrieved").format(n,)) From d7c095173d2f580b9ef1e3dcff1f803a58d6fbf3 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 1 Nov 2018 15:04:26 -0600 Subject: [PATCH 60/75] Improved autoreading functions for #221. Closes #271 --- src/controller/buffers/twitterBuffers.py | 55 +++++++++++++++++++----- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/controller/buffers/twitterBuffers.py b/src/controller/buffers/twitterBuffers.py index 36cb8cb9..5de86072 100644 --- a/src/controller/buffers/twitterBuffers.py +++ b/src/controller/buffers/twitterBuffers.py @@ -69,6 +69,7 @@ class baseBufferController(baseBuffers.buffer): basic_buffers = dict(home_timeline=_(u"Home"), mentions=_(u"Mentions"), direct_messages=_(u"Direct messages"), sent_direct_messages=_(u"Sent direct messages"), sent_tweets=_(u"Sent tweets"), favourites=_(u"Likes"), followers=_(u"Followers"), friends=_(u"Friends"), blocked=_(u"Blocked users"), muted=_(u"Muted users")) if self.name in basic_buffers.keys(): return basic_buffers[self.name] + return "unknown buffer" def post_status(self, *args, **kwargs): title = _(u"Tweet") @@ -176,12 +177,12 @@ class baseBufferController(baseBuffers.buffer): def auto_read(self, number_of_items): if number_of_items == 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: if self.session.settings["general"]["reverse_timelines"] == False: - tweet = self.session.db[self.name][0] - else: tweet = self.session.db[self.name][-1] - output.speak(" ".join(tweet[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"]) + else: + tweet = self.session.db[self.name][0] + output.speak(" ".join(self.compose_function(tweet, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session))) elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: - output.speak(_(u"{0} tweets in {1}.").format(number_of_items, self.get_buffer_name())) + output.speak(_(u"{0} new tweets in {1}.").format(number_of_items, self.get_buffer_name())) def get_more_items(self): elements = [] @@ -695,6 +696,16 @@ class directMessagesController(baseBufferController): self.session.db[self.name]["items"] = [] self.buffer.list.clear() + def auto_read(self, number_of_items): + if number_of_items == 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: + if self.session.settings["general"]["reverse_timelines"] == False: + tweet = self.session.db[self.name]["items"][-1] + else: + tweet = self.session.db[self.name]["items"][0] + output.speak(" ".join(self.compose_function(tweet, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session))) + elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: + output.speak(_(u"{0} new direct messages.").format(number_of_items,)) + class sentDirectMessagesController(directMessagesController): def __init__(self, *args, **kwargs): @@ -725,9 +736,9 @@ class listBufferController(baseBufferController): self.list_id = list_id self.kwargs["list_id"] = list_id - def start_stream(self, mandatory=False, play_sound=True): + def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False): self.get_user_ids() - super(listBufferController, self).start_stream(mandatory, play_sound) + super(listBufferController, self).start_stream(mandatory, play_sound, avoid_autoreading) def get_user_ids(self): next_cursor = -1 @@ -830,7 +841,7 @@ class peopleBufferController(baseBufferController): call_threaded(self.session.api_call, call_name="update_status_with_media", _sound="reply_send.ogg", status=message.message.get_text(), media=message.file) if hasattr(message.message, "destroy"): message.message.destroy() - def start_stream(self, mandatory=False, play_sound=True): + def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False): # starts stream every 3 minutes. current_time = time.time() if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True: @@ -844,6 +855,9 @@ class peopleBufferController(baseBufferController): self.finished_timeline = True if val > 0 and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True: self.session.sound.play(self.sound) + # Autoread settings + if avoid_autoreading == False and mandatory == True and val > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]: + self.auto_read(val) return val def get_more_items(self): @@ -932,8 +946,18 @@ class peopleBufferController(baseBufferController): def details(self, *args, **kwargs): pub.sendMessage("execute-action", action="user_details") + def auto_read(self, number_of_items): + if number_of_items == 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: + if self.session.settings["general"]["reverse_timelines"] == False: + tweet = self.session.db[self.name]["items"][-1] + else: + tweet = self.session.db[self.name["items"]][0] + output.speak(" ".join(self.compose_function(tweet, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session))) + elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: + output.speak(_(u"{0} new followers.").format(number_of_items)) + class searchBufferController(baseBufferController): - def start_stream(self, mandatory=False, play_sound=True): + def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False): # starts stream every 3 minutes. current_time = time.time() if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True: @@ -949,6 +973,9 @@ class searchBufferController(baseBufferController): self.put_items_on_list(num) if num > 0 and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True: self.session.sound.play(self.sound) + # Autoread settings + if avoid_autoreading == False and mandatory == True and num > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]: + self.auto_read(num) return num def remove_buffer(self, force=False): @@ -1014,7 +1041,7 @@ class searchPeopleBufferController(peopleBufferController): if self.kwargs.has_key("page") == False: self.kwargs["page"] = 1 - def start_stream(self, mandatory=False, play_sound=True): + def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=True): # starts stream every 3 minutes. current_time = time.time() if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True: @@ -1031,6 +1058,9 @@ class searchPeopleBufferController(peopleBufferController): self.put_items_on_list(number_of_items) if number_of_items > 0 and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True: self.session.sound.play(self.sound) + # Autoread settings + if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]: + self.auto_read(number_of_items) return number_of_items def get_more_items(self, *args, **kwargs): @@ -1097,7 +1127,7 @@ class trendsBufferController(baseBuffers.buffer): self.get_formatted_message = self.get_message self.reply = self.search_topic - def start_stream(self, mandatory=False, play_sound=True): + def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False): # starts stream every 3 minutes. current_time = time.time() if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory == True: @@ -1202,7 +1232,7 @@ class trendsBufferController(baseBuffers.buffer): class conversationBufferController(searchBufferController): - def start_stream(self, start=False, mandatory=False, play_sound=True): + def start_stream(self, start=False, mandatory=False, play_sound=True, avoid_autoreading=False): # starts stream every 3 minutes. current_time = time.time() if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory == True: @@ -1234,6 +1264,9 @@ class conversationBufferController(searchBufferController): self.put_items_on_list(number_of_items) if number_of_items > 0 and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True: self.session.sound.play(self.sound) + # Autoread settings + if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]: + self.auto_read(number_of_items) return number_of_items def remove_buffer(self, force=False): From 9d2cf05a41612ec0cb90890eaab891d74e6ecbb1 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Nov 2018 15:34:45 -0600 Subject: [PATCH 61/75] Added more docstrings to buffers code --- src/controller/buffers/__init__.py | 6 ++++++ src/controller/buffers/baseBuffers.py | 14 ++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/controller/buffers/__init__.py b/src/controller/buffers/__init__.py index 40a96afc..4e795946 100644 --- a/src/controller/buffers/__init__.py +++ b/src/controller/buffers/__init__.py @@ -1 +1,7 @@ # -*- coding: utf-8 -*- +""" this package contains logic related to buffers. A buffer is a virtual representation of a group of items retrieved through the Social network API'S. + Ideally, new social networks added to TWBlue will have its own "buffers", and these buffers should be defined within this package, following the Twitter example. + Currently, the package contains the following modules: + * baseBuffers: Define a set of functions and structure to be expected in all buffers. New buffers should inherit its classes from one of the classes present here. + * twitterBuffers: All other code, specific to Twitter. +""" \ No newline at end of file diff --git a/src/controller/buffers/baseBuffers.py b/src/controller/buffers/baseBuffers.py index fe469205..3af495cc 100644 --- a/src/controller/buffers/baseBuffers.py +++ b/src/controller/buffers/baseBuffers.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- +""" Common logic to all buffers in TWBlue.""" +import logging import wx import output import config import sound import widgetUtils -import logging from pubsub import pub from wxUI import buffers -log = logging.getLogger("controller.buffers") +log = logging.getLogger("controller.buffers.baseBuffers") def _items_exist(function): """ A decorator to execute a function only if the selected buffer contains at least one item.""" @@ -24,13 +25,12 @@ class buffer(object): """Inits the main controller for this buffer: @ parent wx.Treebook object: Container where we will put this buffer. @ function str or None: function to be called periodically and update items on this buffer. - @ session sessionmanager.session object or None: Session handler for settings, database and Twitter access. + @ session sessionmanager.session object or None: Session handler for settings, database and data access. """ super(buffer, self).__init__() self.function = function # Compose_function will be used to render an object on this buffer. Normally, signature is as follows: # compose_function(item, db, relative_times, show_screen_names=False, session=None) - # Compose functions will be defined in every buffer if items are different than tweets. # Read more about compose functions in twitter/compose.py. self.compose_function = None self.args = args @@ -39,7 +39,7 @@ class buffer(object): self.buffer = None # This should countains the account associated to this buffer. self.account = "" - # This controls wether the start_stream function should be called when starting the program. + # This controls whether the start_stream function should be called when starting the program. self.needs_init = True # if this is set to False, the buffer will be ignored on the invisible interface. self.invisible = False @@ -50,7 +50,7 @@ class buffer(object): pass def get_event(self, ev): - """ Catches key presses in the WX interface and generate the corresponding event names.""" + """ Catch key presses in the WX interface and generate the corresponding event names.""" if ev.GetKeyCode() == wx.WXK_RETURN and ev.ControlDown(): event = "audio" elif ev.GetKeyCode() == wx.WXK_RETURN: event = "url" elif ev.GetKeyCode() == wx.WXK_F5: event = "volume_down" @@ -67,6 +67,7 @@ class buffer(object): pass def volume_down(self): + """ Decreases volume by 5%""" if self.session.settings["sound"]["volume"] > 0.0: if self.session.settings["sound"]["volume"] <= 0.05: self.session.settings["sound"]["volume"] = 0.0 @@ -77,6 +78,7 @@ class buffer(object): self.session.settings.write() def volume_up(self): + """ Increases volume by 5%.""" if self.session.settings["sound"]["volume"] < 1.0: if self.session.settings["sound"]["volume"] >= 0.95: self.session.settings["sound"]["volume"] = 1.0 From 7e42a096a5e5b8e019e83ba898a73d3bdcccee5f Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Nov 2018 16:33:17 -0600 Subject: [PATCH 62/75] Finished implementing autoreading features. Closes #221 --- doc/changelog.md | 1 + src/controller/buffers/twitterBuffers.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/changelog.md b/doc/changelog.md index 4f3e6ede..97ab0ee7 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* autoreading has been redesigned to work in a similar way for almost all buffers. Needs testing. ([#221](https://github.com/manuelcortez/TWBlue/issues/221)) * When displaying tweets or direct messages, a new field has been added to show the date when the item has been posted to Twitter. * Added support for deleting direct messages by using the new Twitter API methods. * When quoting a retweet, the quote will be made to the original tweet instead of the retweet. diff --git a/src/controller/buffers/twitterBuffers.py b/src/controller/buffers/twitterBuffers.py index 5de86072..e03e9398 100644 --- a/src/controller/buffers/twitterBuffers.py +++ b/src/controller/buffers/twitterBuffers.py @@ -69,7 +69,18 @@ class baseBufferController(baseBuffers.buffer): basic_buffers = dict(home_timeline=_(u"Home"), mentions=_(u"Mentions"), direct_messages=_(u"Direct messages"), sent_direct_messages=_(u"Sent direct messages"), sent_tweets=_(u"Sent tweets"), favourites=_(u"Likes"), followers=_(u"Followers"), friends=_(u"Friends"), blocked=_(u"Blocked users"), muted=_(u"Muted users")) if self.name in basic_buffers.keys(): return basic_buffers[self.name] - return "unknown buffer" + # Check user timelines + elif hasattr(self, "username"): + if "-timeline" in self.name: + return _(u"{username}'s timeline").format(username=self.username,) + elif "-favorite" in self.name: + return _(u"{username}'s likes").format(username=self.username,) + elif "-followers" in self.name: + return _(u"{username}'s followers").format(username=self.username,) + elif "-friends" in self.name: + return _(u"{username}'s friends").format(username=self.username,) + log.error("Error getting name for buffer %s" % (self.name,)) + return _(u"Unknown buffer") def post_status(self, *args, **kwargs): title = _(u"Tweet") @@ -180,6 +191,7 @@ class baseBufferController(baseBuffers.buffer): tweet = self.session.db[self.name][-1] else: tweet = self.session.db[self.name][0] + output.speak(_(u"New tweet in {0}").format(self.get_buffer_name())) output.speak(" ".join(self.compose_function(tweet, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session))) elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: output.speak(_(u"{0} new tweets in {1}.").format(number_of_items, self.get_buffer_name())) @@ -702,6 +714,7 @@ class directMessagesController(baseBufferController): tweet = self.session.db[self.name]["items"][-1] else: tweet = self.session.db[self.name]["items"][0] + output.speak(_(u"New direct message")) output.speak(" ".join(self.compose_function(tweet, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session))) elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False: output.speak(_(u"{0} new direct messages.").format(number_of_items,)) From 6f2e439ddcb822d7330de73874f8fa03b4751c41 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Nov 2018 17:16:51 -0600 Subject: [PATCH 63/75] Invert slider movement when up/down arrows are pressed. Fixes #261 --- doc/changelog.md | 1 + src/wxUI/dialogs/configuration.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index 97ab0ee7..8a1049af 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* The volume slider, located in the account settings of TWBlue, now should decrease and increase value properly when up and down arrows are pressed. Before it was doing it in inverted order. ([#261](https://github.com/manuelcortez/TWBlue/issues/261)) * autoreading has been redesigned to work in a similar way for almost all buffers. Needs testing. ([#221](https://github.com/manuelcortez/TWBlue/issues/221)) * When displaying tweets or direct messages, a new field has been added to show the date when the item has been posted to Twitter. * Added support for deleting direct messages by using the new Twitter API methods. diff --git a/src/wxUI/dialogs/configuration.py b/src/wxUI/dialogs/configuration.py index df7560cd..3e7e5ff9 100644 --- a/src/wxUI/dialogs/configuration.py +++ b/src/wxUI/dialogs/configuration.py @@ -4,6 +4,7 @@ import wx import application import output import config +import widgetUtils import baseDialog from multiplatform_widgets import widgets @@ -268,6 +269,9 @@ class sound(wx.Panel): sizer = wx.BoxSizer(wx.VERTICAL) volume = wx.StaticText(self, -1, _(u"Volume")) self.volumeCtrl = wx.Slider(self) + # Connect a key handler here to handle volume slider being inverted when moving with up and down arrows. + # see https://github.com/manuelcortez/TWBlue/issues/261 + widgetUtils.connect_event(self.volumeCtrl, widgetUtils.KEYPRESS, self.on_keypress) self.volumeCtrl.SetRange(0, 100) self.volumeCtrl.SetSize(self.volumeCtrl.GetBestSize()) volumeBox = wx.BoxSizer(wx.HORIZONTAL) @@ -305,6 +309,18 @@ class sound(wx.Panel): sizer.Add(self.indicate_img, 0, wx.ALL, 5) self.SetSizer(sizer) + def on_keypress(self, event, *args, **kwargs): + """ Invert movement of up and down arrow keys when dealing with a wX Slider. + See https://github.com/manuelcortez/TWBlue/issues/261 + and http://trac.wxwidgets.org/ticket/2068 + """ + keycode = event.GetKeyCode() + if keycode == wx.WXK_UP: + return self.volumeCtrl.SetValue(self.volumeCtrl.GetValue()+1) + elif keycode == wx.WXK_DOWN: + return self.volumeCtrl.SetValue(self.volumeCtrl.GetValue()-1) + event.Skip() + def get(self, control): return getattr(self, control).GetStringSelection() From 2c64805eecf3717cb57330c8453e8d5a769d6260 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Tue, 13 Nov 2018 17:32:12 -0600 Subject: [PATCH 64/75] Show list manager action in keystroke editor. Fixes #260 --- doc/changelog.md | 1 + src/keystrokeEditor/constants.py | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index 8a1049af..94c38523 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* Fixed an issue that was making the list manager keystroke unable to be shown in the keystroke editor. Now the keystroke is listed properly. ([#260](https://github.com/manuelcortez/TWBlue/issues/260)) * The volume slider, located in the account settings of TWBlue, now should decrease and increase value properly when up and down arrows are pressed. Before it was doing it in inverted order. ([#261](https://github.com/manuelcortez/TWBlue/issues/261)) * autoreading has been redesigned to work in a similar way for almost all buffers. Needs testing. ([#221](https://github.com/manuelcortez/TWBlue/issues/221)) * When displaying tweets or direct messages, a new field has been added to show the date when the item has been posted to Twitter. diff --git a/src/keystrokeEditor/constants.py b/src/keystrokeEditor/constants.py index 0751d9e5..1512cf10 100644 --- a/src/keystrokeEditor/constants.py +++ b/src/keystrokeEditor/constants.py @@ -50,6 +50,7 @@ actions = { "check_for_updates": _(u"Check and download updates"), "lists_manager": _(u"Opens the list manager, which allows you to create, edit, delete and open lists in buffers."), "configuration": _(u"Opens the global settings dialogue"), +"list_manager": _(u"Opens the list manager"), "accountConfiguration": _(u"Opens the account settings dialogue"), "audio": _(u"Try to play an audio file"), "update_buffer": _(u"Updates the buffer and retrieves possible lost items there."), From 85e575386e6a46858dfa41736f2e25168044f28b Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 15 Nov 2018 04:43:16 -0600 Subject: [PATCH 65/75] Long and quoted tweets are displayed properly in sent tweets. Fixes #253 --- doc/changelog.md | 1 + src/controller/buffers/twitterBuffers.py | 4 ++-- src/controller/mainController.py | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 94c38523..07131d12 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* Quoted and long tweets are displayed properly in the sent tweets buffer after being send. ([#253](https://github.com/manuelcortez/TWBlue/issues/253)) * Fixed an issue that was making the list manager keystroke unable to be shown in the keystroke editor. Now the keystroke is listed properly. ([#260](https://github.com/manuelcortez/TWBlue/issues/260)) * The volume slider, located in the account settings of TWBlue, now should decrease and increase value properly when up and down arrows are pressed. Before it was doing it in inverted order. ([#261](https://github.com/manuelcortez/TWBlue/issues/261)) * autoreading has been redesigned to work in a similar way for almost all buffers. Needs testing. ([#221](https://github.com/manuelcortez/TWBlue/issues/221)) diff --git a/src/controller/buffers/twitterBuffers.py b/src/controller/buffers/twitterBuffers.py index e03e9398..5b9701c0 100644 --- a/src/controller/buffers/twitterBuffers.py +++ b/src/controller/buffers/twitterBuffers.py @@ -504,8 +504,8 @@ class baseBufferController(baseBuffers.buffer): if retweet.image == None: item = self.session.api_call(call_name="update_status", _sound="retweet_send.ogg", status=text, in_reply_to_status_id=id, tweet_mode="extended") if item != None: - item = self.session.twitter.show_status(id=item["id"], include_ext_alt_text=True, tweet_mode="extended") - pub.sendMessage("sent-tweet", data=item, user=self.session.db["user_name"]) + new_item = self.session.twitter.show_status(id=item["id"], include_ext_alt_text=True, tweet_mode="extended") + pub.sendMessage("sent-tweet", data=new_item, user=self.session.db["user_name"]) else: call_threaded(self.session.api_call, call_name="update_status", _sound="retweet_send.ogg", status=text, media=retweet.image) if hasattr(retweet.message, "destroy"): retweet.message.destroy() diff --git a/src/controller/mainController.py b/src/controller/mainController.py index e9d9f802..74728c43 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -1262,6 +1262,10 @@ class Controller(object): if buffer == None: return # if "sent_tweets" not in buffer.session.settings["other_buffers"]["muted_buffers"]: # self.notify(buffer.session, play_sound=play_sound) + data = buffer.session.check_quoted_status(data) + data = buffer.session.check_long_tweet(data) + if data == False: # Long tweet deleted from twishort. + return if buffer.session.settings["general"]["reverse_timelines"] == False: buffer.session.db[buffer.name].append(data) else: From a1a084bfdaa574f676039ea62c53d0398fcc13fc Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 15 Nov 2018 04:49:48 -0600 Subject: [PATCH 66/75] Fixed a small typo --- doc/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.md b/doc/changelog.md index 07131d12..93d28da7 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -17,7 +17,7 @@ * When there are no more items to retrieve in direct messages and people buffers, a message will announce it. * Fixed an issue reported by some users that was making them unable to load more items in their direct messages. * It is possible to add a tweet to the likes buffer from the menu bar again. -* Tweets, replies and retweets will be added to sent tweets right after being posted in the Twitter. +* Tweets, replies and retweets will be added to sent tweets right after being posted in Twitter. * Extended Tweets should be displayed properly in list buffers. ## Changes in version 0.94 From fffd98e09ee5fc7ac31e92447337afe5ffc2d99f Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 17 Nov 2018 13:40:59 -0600 Subject: [PATCH 67/75] Pressing enter in the list will work when adding or removing someone from a list --- doc/changelog.md | 1 + src/wxUI/dialogs/lists.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/doc/changelog.md b/doc/changelog.md index 93d28da7..ebb67885 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* When adding or removing an user from a list, it is possible to press enter in the focused list instead of having to search for the "add" or "delete" button. * Quoted and long tweets are displayed properly in the sent tweets buffer after being send. ([#253](https://github.com/manuelcortez/TWBlue/issues/253)) * Fixed an issue that was making the list manager keystroke unable to be shown in the keystroke editor. Now the keystroke is listed properly. ([#260](https://github.com/manuelcortez/TWBlue/issues/260)) * The volume slider, located in the account settings of TWBlue, now should decrease and increase value properly when up and down arrows are pressed. Before it was doing it in inverted order. ([#261](https://github.com/manuelcortez/TWBlue/issues/261)) diff --git a/src/wxUI/dialogs/lists.py b/src/wxUI/dialogs/lists.py index a5914211..5fb7d57f 100644 --- a/src/wxUI/dialogs/lists.py +++ b/src/wxUI/dialogs/lists.py @@ -113,6 +113,13 @@ class addUserListDialog(listViewer): # self.subscriptors.Disable() # self.members.Disable() self.deleteBtn.Disable() + widgetUtils.connect_event(self.lista.list, widgetUtils.KEYPRESS, self.on_keypress) + + def on_keypress(self, event): + """Catch return and execute ok()""" + if event.GetKeyCode() == wx.WXK_RETURN: + return self.ok() + event.Skip() def ok(self, *args, **kwargs): self.EndModal(wx.ID_OK) @@ -129,6 +136,13 @@ class removeUserListDialog(listViewer): # self.subscriptors.Disable() # self.members.Disable() self.deleteBtn.Disable() + widgetUtils.connect_event(self.lista.list, widgetUtils.KEYPRESS, self.on_keypress) + + def on_keypress(self, event): + """Catch return and execute EndModal()""" + if event.GetKeyCode() == wx.WXK_RETURN: + return self.EndModal(wx.ID_OK) + event.Skip() def remove_list(): return wx.MessageDialog(None, _("Do you really want to delete this list?"), _("Delete"), wx.YES_NO).ShowModal() From 4c1cad7f61d76673c723bc6cda853ca215bba7d4 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 18 Nov 2018 05:22:20 -0600 Subject: [PATCH 68/75] Updated snapshot information --- src/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/application.py b/src/application.py index 04ad4aa1..ce9facbc 100644 --- a/src/application.py +++ b/src/application.py @@ -8,7 +8,7 @@ if snapshot == False: update_url = 'https://twblue.es/updates/stable.php' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/stable.json' else: - version = "9" + version = "11" update_url = 'https://twblue.es/updates/snapshot.php' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/snapshots.json' authors = [u"Manuel Cortéz", u"José Manuel Delicado"] From c5e9e97c84b170c7f893e8d4b63f2346c1218d68 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 18 Nov 2018 06:33:33 -0600 Subject: [PATCH 69/75] Keep custom buffer ordering across restarts and changes in settings --- doc/changelog.md | 1 + src/controller/settings.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index ebb67885..9de89351 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* Custom buffer ordering will not be reseted every time the application restarts after an account setting has been modified. * When adding or removing an user from a list, it is possible to press enter in the focused list instead of having to search for the "add" or "delete" button. * Quoted and long tweets are displayed properly in the sent tweets buffer after being send. ([#253](https://github.com/manuelcortez/TWBlue/issues/253)) * Fixed an issue that was making the list manager keystroke unable to be shown in the keystroke editor. Now the keystroke is listed properly. ([#260](https://github.com/manuelcortez/TWBlue/issues/260)) diff --git a/src/controller/settings.py b/src/controller/settings.py index 96dd4cab..513b3da9 100644 --- a/src/controller/settings.py +++ b/src/controller/settings.py @@ -211,7 +211,7 @@ class accountSettingsController(globalSettingsController): else: self.config["general"]["retweet_mode"] = "comment" buffers_list = self.dialog.buffers.get_list() - if set(self.config["general"]["buffer_order"]) != set(buffers_list) or buffers_list != self.config["general"]["buffer_order"]: + if buffers_list != self.config["general"]["buffer_order"]: self.needs_restart = True self.config["general"]["buffer_order"] = buffers_list self.config["reporting"]["speech_reporting"] = self.dialog.get_value("reporting", "speech_reporting") @@ -291,10 +291,14 @@ class accountSettingsController(globalSettingsController): all_buffers['muted']=_(u"Muted users") list_buffers = [] hidden_buffers=[] - for i in all_buffers.keys(): - if i in self.config["general"]["buffer_order"]: + all_buffers_keys = all_buffers.keys() + # Check buffers shown first. + for i in self.config["general"]["buffer_order"]: + if i in all_buffers_keys: list_buffers.append((i, all_buffers[i], True)) - else: + # This second pass will retrieve all hidden buffers. + for i in all_buffers_keys: + if i not in self.config["general"]["buffer_order"]: hidden_buffers.append((i, all_buffers[i], False)) list_buffers.extend(hidden_buffers) return list_buffers From 4391e3d3de770a4a140158efecc98ce9612c0072 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 22 Nov 2018 12:18:38 -0600 Subject: [PATCH 70/75] Install Twython from a git repo instead of shipping it in the source code. #273 --- requirements.txt | 1 + src/twython/__init__.py | 29 - src/twython/advisory.py | 22 - src/twython/api.py | 708 ----------------- src/twython/compat.py | 40 - src/twython/endpoints.py | 1196 ----------------------------- src/twython/exceptions.py | 61 -- src/twython/helpers.py | 34 - src/twython/streaming/__init__.py | 1 - src/twython/streaming/api.py | 201 ----- src/twython/streaming/types.py | 108 --- 11 files changed, 1 insertion(+), 2400 deletions(-) delete mode 100644 src/twython/__init__.py delete mode 100644 src/twython/advisory.py delete mode 100644 src/twython/api.py delete mode 100644 src/twython/compat.py delete mode 100644 src/twython/endpoints.py delete mode 100644 src/twython/exceptions.py delete mode 100644 src/twython/helpers.py delete mode 100644 src/twython/streaming/__init__.py delete mode 100644 src/twython/streaming/api.py delete mode 100644 src/twython/streaming/types.py diff --git a/requirements.txt b/requirements.txt index 38d64f93..c74b337e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,4 @@ python-vlc pywin32 certifi backports.functools_lru_cache +git+https://github.com/manuelcortez/twython \ No newline at end of file diff --git a/src/twython/__init__.py b/src/twython/__init__.py deleted file mode 100644 index dc161d13..00000000 --- a/src/twython/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# ______ __ __ -# /_ __/_ __ __ __ / /_ / /_ ____ ____ -# / / | | /| / // / / // __// __ \ / __ \ / __ \ -# / / | |/ |/ // /_/ // /_ / / / // /_/ // / / / -# /_/ |__/|__/ \__, / \__//_/ /_/ \____//_/ /_/ -# /____/ - -""" -Twython -------- - -Twython is a library for Python that wraps the Twitter API. - -It aims to abstract away all the API endpoints, so that -additions to the library and/or the Twitter API won't -cause any overall problems. - -Questions, comments? ryan@venodesigns.net -""" - -__author__ = 'Ryan McGrath ' -__version__ = '3.7.0' - -from .api import Twython -from .streaming import TwythonStreamer -from .exceptions import ( - TwythonError, TwythonRateLimitError, TwythonAuthError, - TwythonStreamError -) diff --git a/src/twython/advisory.py b/src/twython/advisory.py deleted file mode 100644 index 31657ee6..00000000 --- a/src/twython/advisory.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.advisory -~~~~~~~~~~~~~~~~ - -This module contains Warning classes for Twython to specifically -alert the user about. - -This mainly is because Python 2.7 > mutes DeprecationWarning and when -we deprecate a method or variable in Twython, we want to use to see -the Warning but don't want ALL DeprecationWarnings to appear, -only TwythonDeprecationWarnings. -""" - - -class TwythonDeprecationWarning(DeprecationWarning): - """Custom DeprecationWarning to be raised when methods/variables - are being deprecated in Twython. Python 2.7 > ignores DeprecationWarning - so we want to specifcally bubble up ONLY Twython Deprecation Warnings - """ - pass diff --git a/src/twython/api.py b/src/twython/api.py deleted file mode 100644 index 96d1ae72..00000000 --- a/src/twython/api.py +++ /dev/null @@ -1,708 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.api -~~~~~~~~~~~ - -This module contains functionality for access to core Twitter API calls, -Twitter Authentication, and miscellaneous methods that are useful when -dealing with the Twitter API -""" - -import warnings -import re - -import requests -from requests.auth import HTTPBasicAuth -from requests_oauthlib import OAuth1, OAuth2 - -from . import __version__ -from .advisory import TwythonDeprecationWarning -from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2 -from .compat import urlsplit -from .endpoints import EndpointsMixin -from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError -from .helpers import _transparent_params - -warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > - - -class Twython(EndpointsMixin, object): - def __init__(self, app_key=None, app_secret=None, oauth_token=None, - oauth_token_secret=None, access_token=None, - token_type='bearer', oauth_version=1, api_version='1.1', - client_args=None, auth_endpoint='authenticate'): - """Instantiates an instance of Twython. Takes optional parameters for - authentication and such (see below). - - :param app_key: (optional) Your applications key - :param app_secret: (optional) Your applications secret key - :param oauth_token: (optional) When using **OAuth 1**, combined with - oauth_token_secret to make authenticated calls - :param oauth_token_secret: (optional) When using **OAuth 1** combined - with oauth_token to make authenticated calls - :param access_token: (optional) When using **OAuth 2**, provide a - valid access token if you have one - :param token_type: (optional) When using **OAuth 2**, provide your - token type. Default: bearer - :param oauth_version: (optional) Choose which OAuth version to use. - Default: 1 - :param api_version: (optional) Choose which Twitter API version to - use. Default: 1.1 - - :param client_args: (optional) Accepts some requests Session parameters - and some requests Request parameters. - See http://docs.python-requests.org/en/latest/api/#sessionapi - and requests section below it for details. - [ex. headers, proxies, verify(SSL verification)] - :param auth_endpoint: (optional) Lets you select which authentication - endpoint will use your application. - This will allow the application to have DM access - if the endpoint is 'authorize'. - Default: authenticate. - """ - - # API urls, OAuth urls and API version; needed for hitting that there - # API. - self.api_version = api_version - self.api_url = 'https://api.twitter.com/%s' - - self.app_key = app_key - self.app_secret = app_secret - self.oauth_token = oauth_token - self.oauth_token_secret = oauth_token_secret - self.access_token = access_token - - # OAuth 1 - self.request_token_url = self.api_url % 'oauth/request_token' - self.access_token_url = self.api_url % 'oauth/access_token' - self.authenticate_url = self.api_url % ('oauth/%s' % auth_endpoint) - - if self.access_token: # If they pass an access token, force OAuth 2 - oauth_version = 2 - - self.oauth_version = oauth_version - - # OAuth 2 - if oauth_version == 2: - self.request_token_url = self.api_url % 'oauth2/token' - - self.client_args = client_args or {} - default_headers = {'User-Agent': 'Twython v' + __version__} - if 'headers' not in self.client_args: - # If they didn't set any headers, set our defaults for them - self.client_args['headers'] = default_headers - elif 'User-Agent' not in self.client_args['headers']: - # If they set headers, but didn't include User-Agent.. set - # it for them - self.client_args['headers'].update(default_headers) - - # Generate OAuth authentication object for the request - # If no keys/tokens are passed to __init__, auth=None allows for - # unauthenticated requests, although I think all v1.1 requests - # need auth - auth = None - if oauth_version == 1: - # User Authentication is through OAuth 1 - if self.app_key is not None and self.app_secret is not None: - auth = OAuth1(self.app_key, self.app_secret, - self.oauth_token, self.oauth_token_secret) - - elif oauth_version == 2 and self.access_token: - # Application Authentication is through OAuth 2 - token = {'token_type': token_type, - 'access_token': self.access_token} - auth = OAuth2(self.app_key, token=token) - - self.client = requests.Session() - self.client.auth = auth - - # Make a copy of the client args and iterate over them - # Pop out all the acceptable args at this point because they will - # Never be used again. - client_args_copy = self.client_args.copy() - for k, v in client_args_copy.items(): - if k in ('cert', 'hooks', 'max_redirects', 'proxies'): - setattr(self.client, k, v) - self.client_args.pop(k) # Pop, pop! - - # Headers are always present, so we unconditionally pop them and merge - # them into the session headers. - self.client.headers.update(self.client_args.pop('headers')) - - self._last_call = None - - def __repr__(self): - return '' % (self.app_key) - - def _request(self, url, method='GET', params=None, api_call=None, json_encoded=False): - """Internal request method""" - method = method.lower() - params = params or {} - - func = getattr(self.client, method) - if isinstance(params, dict) and json_encoded == False: - params, files = _transparent_params(params) - else: - params = params - files = list() - - requests_args = {} - for k, v in self.client_args.items(): - # Maybe this should be set as a class variable and only done once? - if k in ('timeout', 'allow_redirects', 'stream', 'verify'): - requests_args[k] = v - - if method == 'get' or method == 'delete': - requests_args['params'] = params - else: - # Check for json_encoded so we will sent params as "data" or "json" - if json_encoded: - data_key = "json" - else: - data_key = "data" - requests_args.update({ - data_key: params, - 'files': files, - }) - try: - response = func(url, **requests_args) - except requests.RequestException as e: - raise TwythonError(str(e)) - - # create stash for last function intel - self._last_call = { - 'api_call': api_call, - 'api_error': None, - 'cookies': response.cookies, - 'headers': response.headers, - 'status_code': response.status_code, - 'url': response.url, - 'content': response.text, - } - - # greater than 304 (not modified) is an error - if response.status_code > 304: - error_message = self._get_error_message(response) - self._last_call['api_error'] = error_message - - ExceptionType = TwythonError - if response.status_code == 429: - # Twitter API 1.1, always return 429 when - # rate limit is exceeded - ExceptionType = TwythonRateLimitError - elif response.status_code == 401 or 'Bad Authentication data' \ - in error_message: - # Twitter API 1.1, returns a 401 Unauthorized or - # a 400 "Bad Authentication data" for invalid/expired - # app keys/user tokens - ExceptionType = TwythonAuthError - - raise ExceptionType( - error_message, - error_code=response.status_code, - retry_after=response.headers.get('X-Rate-Limit-Reset')) - content = '' - try: - if response.status_code == 204: - content = response.content - else: - content = response.json() - except ValueError: - if response.content != '': - raise TwythonError('Response was not valid JSON. \ - Unable to decode.') - - return content - - def _get_error_message(self, response): - """Parse and return the first error message""" - - error_message = 'An error occurred processing your request.' - try: - content = response.json() - # {"errors":[{"code":34,"message":"Sorry, - # that page does not exist"}]} - error_message = content['errors'][0]['message'] - except TypeError: - error_message = content['errors'] - except ValueError: - # bad json data from Twitter for an error - pass - except (KeyError, IndexError): - # missing data so fallback to default message - pass - - return error_message - - def request(self, endpoint, method='GET', params=None, version='1.1', json_encoded=False): - """Return dict of response received from Twitter's API - - :param endpoint: (required) Full url or Twitter API endpoint - (e.g. search/tweets) - :type endpoint: string - :param method: (optional) Method of accessing data, either - GET, POST or DELETE. (default GET) - :type method: string - :param params: (optional) Dict of parameters (if any) accepted - the by Twitter API endpoint you are trying to - access (default None) - :type params: dict or None - :param version: (optional) Twitter API version to access - (default 1.1) - :type version: string - :param json_encoded: (optional) Flag to indicate if this method should send data encoded as json - (default False) - :type json_encoded: bool - - :rtype: dict - """ - - if endpoint.startswith('http://'): - raise TwythonError('api.twitter.com is restricted to SSL/TLS traffic.') - - # In case they want to pass a full Twitter URL - # i.e. https://api.twitter.com/1.1/search/tweets.json - if endpoint.startswith('https://'): - url = endpoint - else: - url = '%s/%s.json' % (self.api_url % version, endpoint) - - content = self._request(url, method=method, params=params, - api_call=url, json_encoded=json_encoded) - - return content - - def get(self, endpoint, params=None, version='1.1'): - """Shortcut for GET requests via :class:`request`""" - return self.request(endpoint, params=params, version=version) - - def post(self, endpoint, params=None, version='1.1', json_encoded=False): - """Shortcut for POST requests via :class:`request`""" - return self.request(endpoint, 'POST', params=params, version=version, json_encoded=json_encoded) - - def delete(self, endpoint, params=None, version='1.1', json_encoded=False): - """Shortcut for delete requests via :class:`request`""" - return self.request(endpoint, 'DELETE', params=params, version=version, json_encoded=json_encoded) - - def get_lastfunction_header(self, header, default_return_value=None): - """Returns a specific header from the last API call - This will return None if the header is not present - - :param header: (required) The name of the header you want to get - the value of - - Most useful for the following header information: - x-rate-limit-limit, - x-rate-limit-remaining, - x-rate-limit-class, - x-rate-limit-reset - - """ - if self._last_call is None: - raise TwythonError('This function must be called after an API call. \ - It delivers header information.') - - return self._last_call['headers'].get(header, default_return_value) - - def get_authentication_tokens(self, callback_url=None, force_login=False, - screen_name=''): - """Returns a dict including an authorization URL, ``auth_url``, to - direct a user to - - :param callback_url: (optional) Url the user is returned to after - they authorize your app (web clients only) - :param force_login: (optional) Forces the user to enter their - credentials to ensure the correct users - account is authorized. - :param screen_name: (optional) If forced_login is set OR user is - not currently logged in, Prefills the username - input box of the OAuth login screen with the - given value - - :rtype: dict - """ - if self.oauth_version != 1: - raise TwythonError('This method can only be called when your \ - OAuth version is 1.0.') - - request_args = {} - if callback_url: - request_args['oauth_callback'] = callback_url - response = self.client.get(self.request_token_url, params=request_args) - - if response.status_code == 401: - raise TwythonAuthError(response.content, - error_code=response.status_code) - elif response.status_code != 200: - raise TwythonError(response.content, - error_code=response.status_code) - - request_tokens = dict(parse_qsl(response.content.decode('utf-8'))) - if not request_tokens: - raise TwythonError('Unable to decode request tokens.') - - oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') \ - == 'true' - - auth_url_params = { - 'oauth_token': request_tokens['oauth_token'], - } - - if force_login: - auth_url_params.update({ - 'force_login': force_login, - 'screen_name': screen_name - }) - - # Use old-style callback argument if server didn't accept new-style - if callback_url and not oauth_callback_confirmed: - auth_url_params['oauth_callback'] = self.callback_url - - request_tokens['auth_url'] = self.authenticate_url + \ - '?' + urlencode(auth_url_params) - - return request_tokens - - def get_authorized_tokens(self, oauth_verifier): - """Returns a dict of authorized tokens after they go through the - :class:`get_authentication_tokens` phase. - - :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN - for non web apps) retrieved from the callback url querystring - :rtype: dict - - """ - if self.oauth_version != 1: - raise TwythonError('This method can only be called when your \ - OAuth version is 1.0.') - - response = self.client.get(self.access_token_url, - params={'oauth_verifier': oauth_verifier}, - headers={'Content-Type': 'application/\ - json'}) - - if response.status_code == 401: - try: - try: - # try to get json - content = response.json() - except AttributeError: # pragma: no cover - # if unicode detected - content = json.loads(response.content) - except ValueError: - content = {} - - raise TwythonError(content.get('error', 'Invalid / expired To \ - ken'), error_code=response.status_code) - - authorized_tokens = dict(parse_qsl(response.content.decode('utf-8'))) - if not authorized_tokens: - raise TwythonError('Unable to decode authorized tokens.') - - return authorized_tokens # pragma: no cover - - def obtain_access_token(self): - """Returns an OAuth 2 access token to make OAuth 2 authenticated - read-only calls. - - :rtype: string - """ - if self.oauth_version != 2: - raise TwythonError('This method can only be called when your \ - OAuth version is 2.0.') - - data = {'grant_type': 'client_credentials'} - basic_auth = HTTPBasicAuth(self.app_key, self.app_secret) - try: - response = self.client.post(self.request_token_url, - data=data, auth=basic_auth) - content = response.content.decode('utf-8') - try: - content = content.json() - except AttributeError: - content = json.loads(content) - access_token = content['access_token'] - except (KeyError, ValueError, requests.exceptions.RequestException): - raise TwythonAuthError('Unable to obtain OAuth 2 access token.') - else: - return access_token - - @staticmethod - def construct_api_url(api_url, **params): - """Construct a Twitter API url, encoded, with parameters - - :param api_url: URL of the Twitter API endpoint you are attempting - to construct - :param \*\*params: Parameters that are accepted by Twitter for the - endpoint you're requesting - :rtype: string - - Usage:: - - >>> from twython import Twython - >>> twitter = Twython() - - >>> api_url = 'https://api.twitter.com/1.1/search/tweets.json' - >>> constructed_url = twitter.construct_api_url(api_url, q='python', - result_type='popular') - >>> print constructed_url - https://api.twitter.com/1.1/search/tweets.json?q=python&result_type=popular - - """ - querystring = [] - params, _ = _transparent_params(params or {}) - params = requests.utils.to_key_val_list(params) - for (k, v) in params: - querystring.append( - '%s=%s' % (Twython.encode(k), quote_plus(Twython.encode(v))) - ) - return '%s?%s' % (api_url, '&'.join(querystring)) - - def search_gen(self, search_query, **params): # pragma: no cover - warnings.warn( - 'This method is deprecated. You should use Twython.cursor instead. \ - [eg. Twython.cursor(Twython.search, q=\'your_query\')]', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.cursor(self.search, q=search_query, **params) - - def cursor(self, function, return_pages=False, **params): - """Returns a generator for results that match a specified query. - - :param function: Instance of a Twython function - (Twython.get_home_timeline, Twython.search) - :param \*\*params: Extra parameters to send with your request - (usually parameters accepted by the Twitter API endpoint) - :rtype: generator - - Usage:: - - >>> from twython import Twython - >>> twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, - OAUTH_TOKEN_SECRET) - - >>> results = twitter.cursor(twitter.search, q='python') - >>> for result in results: - >>> print result - - """ - if not callable(function): - raise TypeError('.cursor() takes a Twython function as its first \ - argument. Did you provide the result of a \ - function call?') - - if not hasattr(function, 'iter_mode'): - raise TwythonError('Unable to create generator for Twython \ - method "%s"' % function.__name__) - - while True: - content = function(**params) - - if not content: - raise StopIteration - - if hasattr(function, 'iter_key'): - results = content.get(function.iter_key) - else: - results = content - - if return_pages: - yield results - else: - for result in results: - yield result - - if function.iter_mode == 'cursor' and \ - content['next_cursor_str'] == '0': - raise StopIteration - - try: - if function.iter_mode == 'id': - # Set max_id in params to one less than lowest tweet id - if hasattr(function, 'iter_metadata'): - # Get supplied next max_id - metadata = content.get(function.iter_metadata) - if 'next_results' in metadata: - next_results = urlsplit(metadata['next_results']) - params = dict(parse_qsl(next_results.query)) - else: - # No more results - raise StopIteration - else: - # Twitter gives tweets in reverse chronological order: - params['max_id'] = str(int(content[-1]['id_str']) - 1) - elif function.iter_mode == 'cursor': - params['cursor'] = content['next_cursor_str'] - except (TypeError, ValueError): # pragma: no cover - raise TwythonError('Unable to generate next page of search \ - results, `page` is not a number.') - except (KeyError, AttributeError): #pragma no cover - raise TwythonError('Unable to generate next page of search \ - results, content has unexpected structure.') - - @staticmethod - def unicode2utf8(text): - try: - if is_py2 and isinstance(text, str): - text = text.encode('utf-8') - except: - pass - return text - - @staticmethod - def encode(text): - if is_py2 and isinstance(text, (str)): - return Twython.unicode2utf8(text) - return str(text) - - @staticmethod - def html_for_tweet(tweet, use_display_url=True, use_expanded_url=False, expand_quoted_status=False): - """Return HTML for a tweet (urls, mentions, hashtags, symbols replaced with links) - - :param tweet: Tweet object from received from Twitter API - :param use_display_url: Use display URL to represent link - (ex. google.com, github.com). Default: True - :param use_expanded_url: Use expanded URL to represent link - (e.g. http://google.com). Default False - - If use_expanded_url is True, it overrides use_display_url. - If use_display_url and use_expanded_url is False, short url will - be used (t.co/xxxxx) - - """ - if 'retweeted_status' in tweet: - tweet = tweet['retweeted_status'] - - if 'extended_tweet' in tweet: - tweet = tweet['extended_tweet'] - - orig_tweet_text = tweet.get('full_text') or tweet['text'] - - display_text_range = tweet.get('display_text_range') or [0, len(orig_tweet_text)] - display_text_start, display_text_end = display_text_range[0], display_text_range[1] - display_text = orig_tweet_text[display_text_start:display_text_end] - prefix_text = orig_tweet_text[0:display_text_start] - suffix_text = orig_tweet_text[display_text_end:len(orig_tweet_text)] - - if 'entities' in tweet: - # We'll put all the bits of replacement HTML and their starts/ends - # in this list: - entities = [] - - # Mentions - if 'user_mentions' in tweet['entities']: - for entity in tweet['entities']['user_mentions']: - temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] - - mention_html = '@%(screen_name)s' % {'screen_name': entity['screen_name']} - - if display_text_start <= temp['start'] <= display_text_end: - temp['replacement'] = mention_html - temp['start'] -= display_text_start - temp['end'] -= display_text_start - entities.append(temp) - else: - # Make the '@username' at the start, before - # display_text, into a link: - sub_expr = r'(?)' + orig_tweet_text[temp['start']:temp['end']] + '(?!)' - prefix_text = re.sub(sub_expr, mention_html, prefix_text) - - # Hashtags - if 'hashtags' in tweet['entities']: - for entity in tweet['entities']['hashtags']: - temp = {} - temp['start'] = entity['indices'][0] - display_text_start - temp['end'] = entity['indices'][1] - display_text_start - - url_html = '#%(hashtag)s' % {'hashtag': entity['text']} - - temp['replacement'] = url_html - entities.append(temp) - - # Symbols - if 'symbols' in tweet['entities']: - for entity in tweet['entities']['symbols']: - temp = {} - temp['start'] = entity['indices'][0] - display_text_start - temp['end'] = entity['indices'][1] - display_text_start - - url_html = '$%(symbol)s' % {'symbol': entity['text']} - - temp['replacement'] = url_html - entities.append(temp) - - # URLs - if 'urls' in tweet['entities']: - for entity in tweet['entities']['urls']: - temp = {} - temp['start'] = entity['indices'][0] - display_text_start - temp['end'] = entity['indices'][1] - display_text_start - - if use_display_url and entity.get('display_url') and not use_expanded_url: - shown_url = entity['display_url'] - elif use_expanded_url and entity.get('expanded_url'): - shown_url = entity['expanded_url'] - else: - shown_url = entity['url'] - - url_html = '%s' % (entity['url'], shown_url) - - if display_text_start <= temp['start'] <= display_text_end: - temp['replacement'] = url_html - entities.append(temp) - else: - suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) - - if 'media' in tweet['entities'] and len(tweet['entities']['media']) > 0: - # We just link to the overall URL for the tweet's media, - # rather than to each individual item. - # So, we get the URL from the first media item: - entity = tweet['entities']['media'][0] - - temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] - - if use_display_url and entity.get('display_url') and not use_expanded_url: - shown_url = entity['display_url'] - elif use_expanded_url and entity.get('expanded_url'): - shown_url = entity['expanded_url'] - else: - shown_url = entity['url'] - - url_html = '%s' % (entity['url'], shown_url) - - if display_text_start <= temp['start'] <= display_text_end: - temp['replacement'] = url_html - entities.append(temp) - else: - suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) - - # Now do all the replacements, starting from the end, so that the - # start/end indices still work: - for entity in sorted(entities, key=lambda e: e['start'], reverse=True): - display_text = display_text[0:entity['start']] + entity['replacement'] + display_text[entity['end']:] - - quote_text = '' - if expand_quoted_status and tweet.get('is_quote_status') and tweet.get('quoted_status'): - quoted_status = tweet['quoted_status'] - quote_text += '

%(quote)s' \ - '%(quote_user_name)s' \ - '@%(quote_user_screen_name)s' \ - '
' % \ - {'quote': Twython.html_for_tweet(quoted_status, use_display_url, use_expanded_url, False), - 'quote_tweet_link': 'https://twitter.com/%s/status/%s' % - (quoted_status['user']['screen_name'], quoted_status['id_str']), - 'quote_user_name': quoted_status['user']['name'], - 'quote_user_screen_name': quoted_status['user']['screen_name']} - - return '%(prefix)s%(display)s%(suffix)s%(quote)s' % { - 'prefix': '%s' % prefix_text if prefix_text else '', - 'display': display_text, - 'suffix': '%s' % suffix_text if suffix_text else '', - 'quote': quote_text - } diff --git a/src/twython/compat.py b/src/twython/compat.py deleted file mode 100644 index 7c049b00..00000000 --- a/src/twython/compat.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.compat -~~~~~~~~~~~~~~ - -This module contains imports and declarations for seamless Python 2 and -Python 3 compatibility. -""" - -import sys - -_ver = sys.version_info - -#: Python 2.x? -is_py2 = (_ver[0] == 2) - -#: Python 3.x? -is_py3 = (_ver[0] == 3) - -try: - import simplejson as json -except ImportError: - import json - -if is_py2: - from urllib import urlencode, quote_plus - from urlparse import parse_qsl, urlsplit - - str = unicode - basestring = basestring - numeric_types = (int, long, float) - - -elif is_py3: - from urllib.parse import urlencode, quote_plus, parse_qsl, urlsplit - - str = str - basestring = (str, bytes) - numeric_types = (int, float) diff --git a/src/twython/endpoints.py b/src/twython/endpoints.py deleted file mode 100644 index 9adf800f..00000000 --- a/src/twython/endpoints.py +++ /dev/null @@ -1,1196 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.endpoints -~~~~~~~~~~~~~~~~~ - -This module provides a mixin for a :class:`Twython ` instance. -Parameters that need to be embedded in the API url just need to be passed -as a keyword argument. - -e.g. Twython.retweet(id=12345) - -Official documentation for Twitter API endpoints can be found at: -https://developer.twitter.com/en/docs/api-reference-index -""" - -import json -import os -import warnings -from io import BytesIO -from time import sleep -#try: - #from StringIO import StringIO -#except ImportError: - #from io import StringIO - -from .advisory import TwythonDeprecationWarning - - -class EndpointsMixin(object): - # Timelines - def get_mentions_timeline(self, **params): - """Returns the 20 most recent mentions (tweets containing a users's - @screen_name) for the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-mentions_timeline - - """ - return self.get('statuses/mentions_timeline', params=params) - get_mentions_timeline.iter_mode = 'id' - - def get_user_timeline(self, **params): - """Returns a collection of the most recent Tweets posted by the user - indicated by the ``screen_name`` or ``user_id`` parameters. - - Docs: - https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline - - """ - return self.get('statuses/user_timeline', params=params) - get_user_timeline.iter_mode = 'id' - - def get_home_timeline(self, **params): - """Returns a collection of the most recent Tweets and retweets - posted by the authenticating user and the users they follow. - - Docs: - https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline - - """ - return self.get('statuses/home_timeline', params=params) - get_home_timeline.iter_mode = 'id' - - def retweeted_of_me(self, **params): - """Returns the most recent tweets authored by the authenticating user - that have been retweeted by others. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweets_of_me - - """ - return self.get('statuses/retweets_of_me', params=params) - retweeted_of_me.iter_mode = 'id' - - # Tweets - def get_retweets(self, **params): - """Returns up to 100 of the first retweets of a given tweet. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id - - """ - return self.get('statuses/retweets/%s' % params.get('id'), - params=params) - - def show_status(self, **params): - """Returns a single Tweet, specified by the ``id`` parameter - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id - - """ - return self.get('statuses/show/%s' % params.get('id'), params=params) - - def lookup_status(self, **params): - """Returns fully-hydrated tweet objects for up to 100 tweets per - request, as specified by comma-separated values passed to the ``id`` - parameter. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-lookup - - """ - return self.post('statuses/lookup', params=params) - - def destroy_status(self, **params): - """Destroys the status specified by the required ``id`` parameter - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id - - """ - return self.post('statuses/destroy/%s' % params.get('id')) - - def update_status(self, **params): - """Updates the authenticating user's current status, also known as tweeting - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update - - """ - return self.post('statuses/update', params=params) - - def retweet(self, **params): - """Retweets a tweet specified by the ``id`` parameter - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id - - """ - return self.post('statuses/retweet/%s' % params.get('id')) - - def update_status_with_media(self, **params): # pragma: no cover - """Updates the authenticating user's current status and attaches media - for upload. In other words, it creates a Tweet with a picture attached. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update_with_media - - """ - warnings.warn( - 'This method is deprecated. You should use Twython.upload_media instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.post('statuses/update_with_media', params=params) - - def upload_media(self, **params): - """Uploads media file to Twitter servers. The file will be available to be attached - to a status for 60 minutes. To attach to a update, pass a list of returned media ids - to the :meth:`update_status` method using the ``media_ids`` param. - - Docs: - https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload - - """ - # https://developer.twitter.com/en/docs/media/upload-media/api-reference/get-media-upload-status - if params and params.get('command', '') == 'STATUS': - return self.get('https://upload.twitter.com/1.1/media/upload.json', params=params) - - return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) - - def create_metadata(self, **params): - """ Adds metadata to a media element, such as image descriptions for visually impaired. - - Docs: - https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create - - """ - params = json.dumps(params) - return self.post("https://upload.twitter.com/1.1/media/metadata/create.json", params=params) - - def upload_video(self, media, media_type, media_category=None, size=None, check_progress=False): - """Uploads video file to Twitter servers in chunks. The file will be available to be attached - to a status for 60 minutes. To attach to a update, pass a list of returned media ids - to the :meth:`update_status` method using the ``media_ids`` param. - - Upload happens in 3 stages: - - INIT call with size of media to be uploaded(in bytes). If this is more than 15mb, twitter will return error. - - APPEND calls each with media chunk. This returns a 204(No Content) if chunk is received. - - FINALIZE call to complete media upload. This returns media_id to be used with status update. - - Twitter media upload api expects each chunk to be not more than 5mb. We are sending chunk of 1mb each. - - Docs: - https://developer.twitter.com/en/docs/media/upload-media/uploading-media/chunked-media-upload - - """ - upload_url = 'https://upload.twitter.com/1.1/media/upload.json' - if not size: - media.seek(0, os.SEEK_END) - size = media.tell() - media.seek(0) - - # Stage 1: INIT call - params = { - 'command': 'INIT', - 'media_type': media_type, - 'total_bytes': size, - 'media_category': media_category - } - response_init = self.post(upload_url, params=params) - media_id = response_init['media_id'] - - # Stage 2: APPEND calls with 1mb chunks - segment_index = 0 - while True: - data = media.read(1*1024*1024) - if not data: - break - media_chunk = BytesIO() - media_chunk.write(data) - media_chunk.seek(0) - - params = { - 'command': 'APPEND', - 'media_id': media_id, - 'segment_index': segment_index, - 'media': media_chunk, - } - self.post(upload_url, params=params) - segment_index += 1 - - # Stage 3: FINALIZE call to complete upload - params = { - 'command': 'FINALIZE', - 'media_id': media_id - } - - response = self.post(upload_url, params=params) - - # Only get the status if explicity asked to - # Default to False - if check_progress: - - # Stage 4: STATUS call if still processing - params = { - 'command': 'STATUS', - 'media_id': media_id - } - - # added code to handle if media_category is NOT set and check_progress=True - # the API will return a NoneType object in this case - try: - processing_state = response.get('processing_info').get('state') - except AttributeError: - return response - - if processing_state: - while (processing_state == 'pending' or processing_state == 'in_progress') : - # get the secs to wait - check_after_secs = response.get('processing_info').get('check_after_secs') - - if check_after_secs: - sleep(check_after_secs) - response = self.get(upload_url, params=params) - # get new state after waiting - processing_state = response.get('processing_info').get('state') - - return response - - def get_oembed_tweet(self, **params): - """Returns information allowing the creation of an embedded - representation of a Tweet on third party sites. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed - - """ - return self.get('statuses/oembed', params=params) - - def get_retweeters_ids(self, **params): - """Returns a collection of up to 100 user IDs belonging to users who - have retweeted the tweet specified by the ``id`` parameter. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweeters-ids - - """ - return self.get('statuses/retweeters/ids', params=params) - get_retweeters_ids.iter_mode = 'cursor' - get_retweeters_ids.iter_key = 'ids' - - # Search - def search(self, **params): - """Returns a collection of relevant Tweets matching a specified query. - - Docs: - https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets - - """ - return self.get('search/tweets', params=params) - search.iter_mode = 'id' - search.iter_key = 'statuses' - search.iter_metadata = 'search_metadata' - - # Direct Messages - def get_direct_messages(self, **params): - """Returns the 20 most recent direct messages sent to the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events - - """ - return self.get('direct_messages/events/list', params=params) - get_direct_messages.iter_mode = 'id' - - def get_sent_messages(self, **params): - """Returns the 20 most recent direct messages sent by the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-sent-message - - """ - return self.get('direct_messages/sent', params=params) - get_sent_messages.iter_mode = 'id' - - def get_direct_message(self, **params): - """Returns a single direct message, specified by an ``id`` parameter. - - Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-event - - """ - return self.get('direct_messages/events/show', params=params) - - def destroy_direct_message(self, **params): - """Destroys the direct message specified in the required ``id`` parameter - - Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message-event - - """ - return self.delete('direct_messages/events/destroy', params=params) - - def send_direct_message(self, **params): - """Sends a new direct message to the specified user from the - authenticating user. - - Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event - - """ - return self.post('direct_messages/events/new', params=params, json_encoded=True) - - # Friends & Followers - def get_user_ids_of_blocked_retweets(self, **params): - """Returns a collection of user_ids that the currently authenticated - user does not want to receive retweets from. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-no_retweets-ids - - """ - return self.get('friendships/no_retweets/ids', params=params) - - def get_friends_ids(self, **params): - """Returns a cursored collection of user IDs for every user the - specified user is following (otherwise known as their "friends"). - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-ids - - """ - return self.get('friends/ids', params=params) - get_friends_ids.iter_mode = 'cursor' - get_friends_ids.iter_key = 'ids' - - def get_followers_ids(self, **params): - """Returns a cursored collection of user IDs for every user - following the specified user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids - - """ - return self.get('followers/ids', params=params) - get_followers_ids.iter_mode = 'cursor' - get_followers_ids.iter_key = 'ids' - - def lookup_friendships(self, **params): - """Returns the relationships of the authenticating user to the - comma-separated list of up to 100 ``screen_names`` or ``user_ids`` provided. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-lookup - - """ - return self.get('friendships/lookup', params=params) - - def get_incoming_friendship_ids(self, **params): - """Returns a collection of numeric IDs for every user who has a - pending request to follow the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-incoming - - """ - return self.get('friendships/incoming', params=params) - get_incoming_friendship_ids.iter_mode = 'cursor' - get_incoming_friendship_ids.iter_key = 'ids' - - def get_outgoing_friendship_ids(self, **params): - """Returns a collection of numeric IDs for every protected user for - whom the authenticating user has a pending follow request. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-outgoing - - """ - return self.get('friendships/outgoing', params=params) - get_outgoing_friendship_ids.iter_mode = 'cursor' - get_outgoing_friendship_ids.iter_key = 'ids' - - def create_friendship(self, **params): - """Allows the authenticating users to follow the user specified - in the ``id`` parameter. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-create - - """ - return self.post('friendships/create', params=params) - - def destroy_friendship(self, **params): - """Allows the authenticating user to unfollow the user specified - in the ``id`` parameter. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy - - """ - return self.post('friendships/destroy', params=params) - - def update_friendship(self, **params): - """Allows one to enable or disable retweets and device notifications - from the specified user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-update - - """ - return self.post('friendships/update', params=params) - - def show_friendship(self, **params): - """Returns detailed information about the relationship between two - arbitrary users. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-show - - """ - return self.get('friendships/show', params=params) - - def get_friends_list(self, **params): - """Returns a cursored collection of user objects for every user the - specified user is following (otherwise known as their "friends"). - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-list - - """ - return self.get('friends/list', params=params) - get_friends_list.iter_mode = 'cursor' - get_friends_list.iter_key = 'users' - - def get_followers_list(self, **params): - """Returns a cursored collection of user objects for users - following the specified user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-list - - """ - return self.get('followers/list', params=params) - get_followers_list.iter_mode = 'cursor' - get_followers_list.iter_key = 'users' - - # Users - def get_account_settings(self, **params): - """Returns settings (including current trend, geo and sleep time - information) for the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-settings - - """ - return self.get('account/settings', params=params) - - def verify_credentials(self, **params): - """Returns an HTTP 200 OK response code and a representation of the - requesting user if authentication was successful; returns a 401 status - code and an error message if not. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials - - """ - return self.get('account/verify_credentials', params=params) - - def update_account_settings(self, **params): - """Updates the authenticating user's settings. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-settings - - """ - return self.post('account/settings', params=params) - - def update_delivery_service(self, **params): - """Sets which device Twitter delivers updates to for the authenticating user. - - Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_delivery_device - - """ - return self.post('account/update_delivery_device', params=params) - - def update_profile(self, **params): - """Sets values that users are able to set under the "Account" tab of their - settings page. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile - - """ - return self.post('account/update_profile', params=params) - - def update_profile_banner_image(self, **params): # pragma: no cover - """Updates the authenticating user's profile background image. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_background_image - - """ - return self.post('account/update_profile_banner', params=params) - - def update_profile_colors(self, **params): # pragma: no cover - """Sets one or more hex values that control the color scheme of the - authenticating user's profile page on twitter.com. - - This method is deprecated, replaced by the ``profile_link_color`` - parameter to :meth:`update_profile`. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile - - """ - warnings.warn( - 'This method is deprecated. You should use the' - ' profile_link_color parameter in Twython.update_profile instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.post('account/update_profile_colors', params=params) - - def update_profile_image(self, **params): # pragma: no cover - """Updates the authenticating user's profile image. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image - - """ - return self.post('account/update_profile_image', params=params) - - def list_blocks(self, **params): - """Returns a collection of user objects that the authenticating user - is blocking. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-list - - """ - return self.get('blocks/list', params=params) - list_blocks.iter_mode = 'cursor' - list_blocks.iter_key = 'users' - - def list_block_ids(self, **params): - """Returns an array of numeric user ids the authenticating user is blocking. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-ids - - """ - return self.get('blocks/ids', params=params) - list_block_ids.iter_mode = 'cursor' - list_block_ids.iter_key = 'ids' - - def create_block(self, **params): - """Blocks the specified user from following the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-create - - """ - return self.post('blocks/create', params=params) - - def destroy_block(self, **params): - """Un-blocks the user specified in the ``id`` parameter for the - authenticating user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-destroy - - """ - return self.post('blocks/destroy', params=params) - - def lookup_user(self, **params): - """Returns fully-hydrated user objects for up to 100 users per request, - as specified by comma-separated values passed to the ``user_id`` and/or - ``screen_name`` parameters. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-lookup - - """ - return self.get('users/lookup', params=params) - - def show_user(self, **params): - """Returns a variety of information about the user specified by the - required ``user_id`` or ``screen_name`` parameter. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show - - """ - return self.get('users/show', params=params) - - def search_users(self, **params): - """Provides a simple, relevance-based search interface to public user - accounts on Twitter. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-search - - """ - return self.get('users/search', params=params) - - def get_contributees(self, **params): - """Returns a collection of users that the specified user can "contribute" to. - - Docs: https://dev.twitter.com/docs/api/1.1/get/users/contributees - - """ - return self.get('users/contributees', params=params) - - def get_contributors(self, **params): - """Returns a collection of users who can contribute to the specified account. - - Docs: https://dev.twitter.com/docs/api/1.1/get/users/contributors - - """ - return self.get('users/contributors', params=params) - - def remove_profile_banner(self, **params): - """Removes the uploaded profile banner for the authenticating user. - Returns HTTP 200 upon success. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-remove_profile_banner - - """ - return self.post('account/remove_profile_banner', params=params) - - def update_profile_background_image(self, **params): - """Uploads a profile banner on behalf of the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_banner - - """ - return self.post('account/update_profile_background_image', - params=params) - - def get_profile_banner_sizes(self, **params): - """Returns a map of the available size variations of the specified - user's profile banner. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-users-profile_banner - - """ - return self.get('users/profile_banner', params=params) - - def list_mutes(self, **params): - """Returns a collection of user objects that the authenticating user - is muting. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-mutes-users-list - - """ - return self.get('mutes/users/list', params=params) - list_mutes.iter_mode = 'cursor' - list_mutes.iter_key = 'users' - - def list_mute_ids(self, **params): - """Returns an array of numeric user ids the authenticating user - is muting. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-mutes-users-ids - - """ - return self.get('mutes/users/ids', params=params) - list_mute_ids.iter_mode = 'cursor' - list_mute_ids.iter_key = 'ids' - - def create_mute(self, **params): - """Mutes the specified user, preventing their tweets appearing - in the authenticating user's timeline. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-create - - """ - return self.post('mutes/users/create', params=params) - - def destroy_mute(self, **params): - """Un-mutes the user specified in the user or ``id`` parameter for - the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-destroy - - """ - return self.post('mutes/users/destroy', params=params) - - # Suggested Users - def get_user_suggestions_by_slug(self, **params): - """Access the users in a given category of the Twitter suggested user list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions-slug - - """ - return self.get('users/suggestions/%s' % params.get('slug'), - params=params) - - def get_user_suggestions(self, **params): - """Access to Twitter's suggested user list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions - - """ - return self.get('users/suggestions', params=params) - - def get_user_suggestions_statuses_by_slug(self, **params): - """Access the users in a given category of the Twitter suggested user - list and return their most recent status if they are not a protected - user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions-slug-members - - """ - return self.get('users/suggestions/%s/members' % params.get('slug'), - params=params) - - # Favorites - def get_favorites(self, **params): - """Returns the 20 most recent Tweets favorited by the authenticating - or specified user. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-favorites-list - - """ - return self.get('favorites/list', params=params) - get_favorites.iter_mode = 'id' - - def destroy_favorite(self, **params): - """Un-favorites the status specified in the ``id`` parameter as the - authenticating user. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-favorites-destroy - - """ - return self.post('favorites/destroy', params=params) - - def create_favorite(self, **params): - """Favorites the status specified in the ``id`` parameter as the - authenticating user. - - Docs: - https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-favorites-create - - """ - return self.post('favorites/create', params=params) - - # Lists - def show_lists(self, **params): - """Returns all lists the authenticating or specified user subscribes to, - including their own. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list - - """ - return self.get('lists/list', params=params) - - def get_list_statuses(self, **params): - """Returns a timeline of tweets authored by members of the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-statuses - - """ - return self.get('lists/statuses', params=params) - get_list_statuses.iter_mode = 'id' - - def delete_list_member(self, **params): - """Removes the specified member from the list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-destroy - - """ - return self.post('lists/members/destroy', params=params) - - def get_list_memberships(self, **params): - """Returns the lists the specified user has been added to. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-memberships - - """ - return self.get('lists/memberships', params=params) - get_list_memberships.iter_mode = 'cursor' - get_list_memberships.iter_key = 'lists' - - def get_list_subscribers(self, **params): - """Returns the subscribers of the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscribers - - """ - return self.get('lists/subscribers', params=params) - get_list_subscribers.iter_mode = 'cursor' - get_list_subscribers.iter_key = 'users' - - def subscribe_to_list(self, **params): - """Subscribes the authenticated user to the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-subscribers-create - - """ - return self.post('lists/subscribers/create', params=params) - - def is_list_subscriber(self, **params): - """Check if the specified user is a subscriber of the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscribers-show - - """ - return self.get('lists/subscribers/show', params=params) - - def unsubscribe_from_list(self, **params): - """Unsubscribes the authenticated user from the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-subscribers-destroy - - """ - return self.post('lists/subscribers/destroy', params=params) - - def create_list_members(self, **params): - """Adds multiple members to a list, by specifying a comma-separated - list of member ids or screen names. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-create_all - - """ - return self.post('lists/members/create_all', params=params) - - def is_list_member(self, **params): - """Check if the specified user is a member of the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-members-show - - """ - return self.get('lists/members/show', params=params) - - def get_list_members(self, **params): - """Returns the members of the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-members - - """ - return self.get('lists/members', params=params) - get_list_members.iter_mode = 'cursor' - get_list_members.iter_key = 'users' - - def add_list_member(self, **params): - """Add a member to a list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-create - - """ - return self.post('lists/members/create', params=params) - - def delete_list(self, **params): - """Deletes the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-destroy - - """ - return self.post('lists/destroy', params=params) - - def update_list(self, **params): - """Updates the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-update - - """ - return self.post('lists/update', params=params) - - def create_list(self, **params): - """Creates a new list for the authenticated user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-create - - """ - return self.post('lists/create', params=params) - - def get_specific_list(self, **params): - """Returns the specified list. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-show - - """ - return self.get('lists/show', params=params) - - def get_list_subscriptions(self, **params): - """Obtain a collection of the lists the specified user is subscribed to. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscriptions - - """ - return self.get('lists/subscriptions', params=params) - get_list_subscriptions.iter_mode = 'cursor' - get_list_subscriptions.iter_key = 'lists' - - def delete_list_members(self, **params): - """Removes multiple members from a list, by specifying a - comma-separated list of member ids or screen names. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-destroy_all - - """ - return self.post('lists/members/destroy_all', params=params) - - def show_owned_lists(self, **params): - """Returns the lists owned by the specified Twitter user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships - - """ - return self.get('lists/ownerships', params=params) - show_owned_lists.iter_mode = 'cursor' - show_owned_lists.iter_key = 'lists' - - # Saved Searches - def get_saved_searches(self, **params): - """Returns the authenticated user's saved search queries. - - Docs: - https://developer.twitter.com/en/docs/tweets/search/api-reference/get-saved_searches-list - - """ - return self.get('saved_searches/list', params=params) - - def show_saved_search(self, **params): - """Retrieve the information for the saved search represented by the given ``id``. - - Docs: - https://developer.twitter.com/en/docs/tweets/search/api-reference/get-saved_searches-show-id - - """ - return self.get('saved_searches/show/%s' % params.get('id'), - params=params) - - def create_saved_search(self, **params): - """Create a new saved search for the authenticated user. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-create - - """ - return self.post('saved_searches/create', params=params) - - def destroy_saved_search(self, **params): - """Destroys a saved search for the authenticating user. - - Docs: - https://developer.twitter.com/en/docs/tweets/search/api-reference/post-saved_searches-destroy-id - - """ - return self.post('saved_searches/destroy/%s' % params.get('id'), - params=params) - - # Places & Geo - def get_geo_info(self, **params): - """Returns all the information about a known place. - - Docs: - https://developer.twitter.com/en/docs/geo/place-information/api-reference/get-geo-id-place_id - - """ - return self.get('geo/id/%s' % params.get('place_id'), params=params) - - def reverse_geocode(self, **params): - """Given a latitude and a longitude, searches for up to 20 places - that can be used as a place_id when updating a status. - - Docs: - https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-reverse_geocode - - """ - return self.get('geo/reverse_geocode', params=params) - - def search_geo(self, **params): - """Search for places that can be attached to a statuses/update. - - Docs: - https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-search - - """ - return self.get('geo/search', params=params) - - def get_similar_places(self, **params): - """Locates places near the given coordinates which are similar in name. - - Docs: https://dev.twitter.com/docs/api/1.1/get/geo/similar_places - - """ - return self.get('geo/similar_places', params=params) - - def create_place(self, **params): # pragma: no cover - """Creates a new place object at the given latitude and longitude. - - Docs: https://dev.twitter.com/docs/api/1.1/post/geo/place - - """ - return self.post('geo/place', params=params) - - # Trends - def get_place_trends(self, **params): - """Returns the top 10 trending topics for a specific WOEID, if - trending information is available for it. - - Docs: - https://developer.twitter.com/en/docs/trends/trends-for-location/api-reference/get-trends-place - - """ - return self.get('trends/place', params=params) - - def get_available_trends(self, **params): - """Returns the locations that Twitter has trending topic information for. - - Docs: - https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-available - - """ - return self.get('trends/available', params=params) - - def get_closest_trends(self, **params): - """Returns the locations that Twitter has trending topic information - for, closest to a specified location. - - Docs: - https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-closest - - """ - return self.get('trends/closest', params=params) - - # Spam Reporting - def report_spam(self, **params): # pragma: no cover - """Report the specified user as a spam account to Twitter. - - Docs: - https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-users-report_spam - - """ - return self.post('users/report_spam', params=params) - - # OAuth - def invalidate_token(self, **params): # pragma: no cover - """Allows a registered application to revoke an issued OAuth 2 Bearer - Token by presenting its client credentials. - - Docs: - https://developer.twitter.com/en/docs/basics/authentication/api-reference/invalidate_token - - """ - return self.post('oauth2/invalidate_token', params=params) - - # Help - def get_twitter_configuration(self, **params): - """Returns the current configuration used by Twitter - - Docs: - https://developer.twitter.com/en/docs/developer-utilities/configuration/api-reference/get-help-configuration - - """ - return self.get('help/configuration', params=params) - - def get_supported_languages(self, **params): - """Returns the list of languages supported by Twitter along with - their ISO 639-1 code. - - Docs: - https://developer.twitter.com/en/docs/developer-utilities/supported-languages/api-reference/get-help-languages - - """ - return self.get('help/languages', params=params) - - def get_privacy_policy(self, **params): - """Returns Twitter's Privacy Policy - - Docs: - https://developer.twitter.com/en/docs/developer-utilities/privacy-policy/api-reference/get-help-privacy - - """ - return self.get('help/privacy', params=params) - - def get_tos(self, **params): - """Return the Twitter Terms of Service - - Docs: - https://developer.twitter.com/en/docs/developer-utilities/terms-of-service/api-reference/get-help-tos - - """ - return self.get('help/tos', params=params) - - def get_application_rate_limit_status(self, **params): - """Returns the current rate limits for methods belonging to the - specified resource families. - - Docs: - https://developer.twitter.com/en/docs/developer-utilities/rate-limit-status/api-reference/get-application-rate_limit_status - - """ - return self.get('application/rate_limit_status', params=params) - - -# from https://developer.twitter.com/en/docs/ads/general/guides/response-codes -TWITTER_HTTP_STATUS_CODE = { - 200: ('OK', 'Success!'), - 304: ('Not Modified', 'There was no new data to return.'), - 400: ('Bad Request', 'The request was invalid. An accompanying \ - error message will explain why. This is the status code \ - will be returned during rate limiting.'), - 401: ('Unauthorized', 'Authentication credentials were missing \ - or incorrect.'), - 403: ('Forbidden', 'The request is understood, but it has been \ - refused. An accompanying error message will explain why. \ - This code is used when requests are being denied due to \ - update limits.'), - 404: ('Not Found', 'The URI requested is invalid or the resource \ - requested, such as a user, does not exists.'), - 406: ('Not Acceptable', 'Returned by the Search API when an \ - invalid format is specified in the request.'), - 410: ('Gone', 'This resource is gone. Used to indicate that an \ - API endpoint has been turned off.'), - 422: ('Unprocessable Entity', 'Returned when an image uploaded to \ - POST account/update_profile_banner is unable to be processed.'), - 429: ('Too Many Requests', 'Returned in API v1.1 when a request cannot \ - be served due to the application\'s rate limit having been \ - exhausted for the resource.'), - 500: ('Internal Server Error', 'Something is broken. Please post to the \ - group so the Twitter team can investigate.'), - 502: ('Bad Gateway', 'Twitter is down or being upgraded.'), - 503: ('Service Unavailable', 'The Twitter servers are up, but overloaded \ - with requests. Try again later.'), - 504: ('Gateway Timeout', 'The Twitter servers are up, but the request \ - couldn\'t be serviced due to some failure within our stack. Try \ - again later.'), -} diff --git a/src/twython/exceptions.py b/src/twython/exceptions.py deleted file mode 100644 index 4aa7dbab..00000000 --- a/src/twython/exceptions.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.exceptions -~~~~~~~~~~~~~~~~~~ - -This module contains Twython specific Exception classes. -""" - -from .endpoints import TWITTER_HTTP_STATUS_CODE - - -class TwythonError(Exception): - """Generic error class, catch-all for most Twython issues. - Special cases are handled by TwythonAuthError & TwythonRateLimitError. - - from twython import TwythonError, TwythonRateLimitError, TwythonAuthError - - """ - def __init__(self, msg, error_code=None, retry_after=None): - self.error_code = error_code - - if error_code is not None and error_code in TWITTER_HTTP_STATUS_CODE: - msg = 'Twitter API returned a %s (%s), %s' % \ - (error_code, - TWITTER_HTTP_STATUS_CODE[error_code][0], - msg) - - super(TwythonError, self).__init__(msg) - - @property - def msg(self): # pragma: no cover - return self.args[0] - - -class TwythonAuthError(TwythonError): - """Raised when you try to access a protected resource and it fails due to - some issue with your authentication. - - """ - pass - - -class TwythonRateLimitError(TwythonError): # pragma: no cover - """Raised when you've hit a rate limit. - - The amount of seconds to retry your request in will be appended - to the message. - - """ - def __init__(self, msg, error_code, retry_after=None): - if isinstance(retry_after, int): - msg = '%s (Retry after %d seconds)' % (msg, retry_after) - TwythonError.__init__(self, msg, error_code=error_code) - - self.retry_after = retry_after - - -class TwythonStreamError(TwythonError): - """Raised when an invalid response from the Stream API is received""" - pass diff --git a/src/twython/helpers.py b/src/twython/helpers.py deleted file mode 100644 index f44d0ce7..00000000 --- a/src/twython/helpers.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.helpers -~~~~~~~~~~~~~~~ - -This module contains functions that are repeatedly used throughout -the Twython library. -""" - -from .compat import basestring, numeric_types - - -def _transparent_params(_params): - params = {} - files = {} - for k, v in _params.items(): - if hasattr(v, 'read') and callable(v.read): - files[k] = v # pragma: no cover - elif isinstance(v, bool): - if v: - params[k] = 'true' - else: - params[k] = 'false' - elif isinstance(v, basestring) or isinstance(v, numeric_types): - params[k] = v - elif isinstance(v, list): - try: - params[k] = ','.join(v) - except TypeError: - params[k] = ','.join(map(str, v)) - else: - continue # pragma: no cover - return params, files diff --git a/src/twython/streaming/__init__.py b/src/twython/streaming/__init__.py deleted file mode 100644 index b12e8d16..00000000 --- a/src/twython/streaming/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .api import TwythonStreamer diff --git a/src/twython/streaming/api.py b/src/twython/streaming/api.py deleted file mode 100644 index 0195f380..00000000 --- a/src/twython/streaming/api.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.streaming.api -~~~~~~~~~~~~~~~~~~~~~ - -This module contains functionality for interfacing with streaming -Twitter API calls. -""" - -from .. import __version__ -from ..compat import json, is_py3 -from ..helpers import _transparent_params -from .types import TwythonStreamerTypes - -import requests -from requests_oauthlib import OAuth1 - -import time - - -class TwythonStreamer(object): - def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret, - timeout=300, retry_count=None, retry_in=10, client_args=None, - handlers=None, chunk_size=1): - """Streaming class for a friendly streaming user experience - Authentication IS required to use the Twitter Streaming API - - :param app_key: (required) Your applications key - :param app_secret: (required) Your applications secret key - :param oauth_token: (required) Used with oauth_token_secret to make - authenticated calls - :param oauth_token_secret: (required) Used with oauth_token to make - authenticated calls - :param timeout: (optional) How long (in secs) the streamer should wait - for a response from Twitter Streaming API - :param retry_count: (optional) Number of times the API call should be - retired - :param retry_in: (optional) Amount of time (in secs) the previous - API call should be tried again - :param client_args: (optional) Accepts some requests Session - parameters and some requests Request parameters. - See - http://docs.python-requests.org/en/latest/api/#sessionapi - and requests section below it for details. - [ex. headers, proxies, verify(SSL verification)] - :param handlers: (optional) Array of message types for which - corresponding handlers will be called - - :param chunk_size: (optional) Define the buffer size before data is - actually returned from the Streaming API. Default: 1 - """ - - self.auth = OAuth1(app_key, app_secret, - oauth_token, oauth_token_secret) - - self.client_args = client_args or {} - default_headers = {'User-Agent': 'Twython Streaming v' + __version__} - if 'headers' not in self.client_args: - # If they didn't set any headers, set our defaults for them - self.client_args['headers'] = default_headers - elif 'User-Agent' not in self.client_args['headers']: - # If they set headers, but didn't include User-Agent.. - # set it for them - self.client_args['headers'].update(default_headers) - self.client_args['timeout'] = timeout - - self.client = requests.Session() - self.client.auth = self.auth - self.client.stream = True - - # Make a copy of the client args and iterate over them - # Pop out all the acceptable args at this point because they will - # Never be used again. - client_args_copy = self.client_args.copy() - for k, v in client_args_copy.items(): - if k in ('cert', 'headers', 'hooks', 'max_redirects', 'proxies'): - setattr(self.client, k, v) - self.client_args.pop(k) # Pop, pop! - - self.api_version = '1.1' - - self.retry_in = retry_in - self.retry_count = retry_count - - # Set up type methods - StreamTypes = TwythonStreamerTypes(self) - self.statuses = StreamTypes.statuses - self.user = StreamTypes.user - self.site = StreamTypes.site - - self.connected = False - - self.handlers = handlers if handlers else \ - ['delete', 'limit', 'disconnect'] - - self.chunk_size = chunk_size - - def _request(self, url, method='GET', params=None): - """Internal stream request handling""" - self.connected = True - retry_counter = 0 - - method = method.lower() - func = getattr(self.client, method) - params, _ = _transparent_params(params) - - def _send(retry_counter): - requests_args = {} - for k, v in self.client_args.items(): - # Maybe this should be set as a class - # variable and only done once? - if k in ('timeout', 'allow_redirects', 'verify'): - requests_args[k] = v - - while self.connected: - try: - if method == 'get': - requests_args['params'] = params - else: - requests_args['data'] = params - - response = func(url, **requests_args) - except requests.exceptions.Timeout: - self.on_timeout() - else: - if response.status_code != 200: - self.on_error(response.status_code, response.content) - - if self.retry_count and \ - (self.retry_count - retry_counter) > 0: - time.sleep(self.retry_in) - retry_counter += 1 - _send(retry_counter) - - return response - - while self.connected: - response = _send(retry_counter) - - for line in response.iter_lines(self.chunk_size): - if not self.connected: - break - if line: - try: - if is_py3: - line = line.decode('utf-8') - data = json.loads(line) - except ValueError: # pragma: no cover - self.on_error(response.status_code, - 'Unable to decode response, \ - not valid JSON.') - else: - if self.on_success(data): # pragma: no cover - for message_type in self.handlers: - if message_type in data: - handler = getattr(self, - 'on_' + message_type, - None) - if handler \ - and callable(handler) \ - and not handler(data.get(message_type)): - break - - response.close() - - def on_success(self, data): # pragma: no cover - """Called when data has been successfully received from the stream. - Returns True if other handlers for this message should be invoked. - - Feel free to override this to handle your streaming data how you - want it handled. See - https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/streaming-message-types - for messages sent along in stream responses. - - :param data: data recieved from the stream - :type data: dict - """ - return True - - def on_error(self, status_code, data): # pragma: no cover - """Called when stream returns non-200 status code - - Feel free to override this to handle your streaming data how you - want it handled. - - :param status_code: Non-200 status code sent from stream - :type status_code: int - - :param data: Error message sent from stream - :type data: dict - """ - return - - def on_timeout(self): # pragma: no cover - """ Called when the request has timed out """ - return - - def disconnect(self): - """Used to disconnect the streaming client manually""" - self.connected = False diff --git a/src/twython/streaming/types.py b/src/twython/streaming/types.py deleted file mode 100644 index 81c5c07f..00000000 --- a/src/twython/streaming/types.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -twython.streaming.types -~~~~~~~~~~~~~~~~~~~~~~~ - -This module contains classes and methods for :class:`TwythonStreamer` to use. -""" - - -class TwythonStreamerTypes(object): - """Class for different stream endpoints - - Not all streaming endpoints have nested endpoints. - User Streams and Site Streams are single streams with no nested endpoints - Status Streams include filter, sample and firehose endpoints - - """ - def __init__(self, streamer): - self.streamer = streamer - self.statuses = TwythonStreamerTypesStatuses(streamer) - - def user(self, **params): - """Stream user - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/user - """ - url = 'https://userstream.twitter.com/%s/user.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - def site(self, **params): - """Stream site - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/site - """ - url = 'https://sitestream.twitter.com/%s/site.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - -class TwythonStreamerTypesStatuses(object): - """Class for different statuses endpoints - - Available so :meth:`TwythonStreamer.statuses.filter()` is available. - Just a bit cleaner than :meth:`TwythonStreamer.statuses_filter()`, - :meth:`statuses_sample()`, etc. all being single methods in - :class:`TwythonStreamer`. - - """ - def __init__(self, streamer): - self.streamer = streamer - self.params = None - - def filter(self, **params): - """Stream statuses/filter - - :param \*\*params: Parameters to send with your stream request - - Accepted params found at: - https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter - """ - url = 'https://stream.twitter.com/%s/statuses/filter.json' \ - % self.streamer.api_version - self.streamer._request(url, 'POST', params=params) - - def sample(self, **params): - """Stream statuses/sample - - :param \*\*params: Parameters to send with your stream request - - Accepted params found at: - https://developer.twitter.com/en/docs/tweets/sample-realtime/api-reference/get-statuses-sample - """ - url = 'https://stream.twitter.com/%s/statuses/sample.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - def firehose(self, **params): - """Stream statuses/firehose - - :param \*\*params: Parameters to send with your stream request - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/statuses/firehose - """ - url = 'https://stream.twitter.com/%s/statuses/firehose.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - def set_dynamic_filter(self, **params): - """Set/update statuses/filter - - :param \*\*params: Parameters to send with your stream request - - Accepted params found at: - https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter - """ - self.params = params - - def dynamic_filter(self): - """Stream statuses/filter with dynamic parameters""" - - url = 'https://stream.twitter.com/%s/statuses/filter.json' \ - % self.streamer.api_version - self.streamer._request(url, 'POST', params=self.params) From 221d1d413ba11614549d7f1df067c884b2239927 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 22 Nov 2018 13:35:19 -0600 Subject: [PATCH 71/75] Changed codebase's syntax before attempt the python3 migration later. #273 --- src/controller/buffers/twitterBuffers.py | 32 +++++------ src/controller/filterController.py | 2 +- src/controller/mainController.py | 19 +++---- src/controller/messages.py | 31 +++++------ src/controller/userActionsController.py | 2 +- src/extra/AudioUploader/audioUploader.py | 7 +-- src/extra/AudioUploader/transfer.py | 7 +-- .../AudioUploader/wx_transfer_dialogs.py | 3 +- src/extra/SoundsTutorial/__init__.py | 3 +- src/extra/SoundsTutorial/soundsTutorial.py | 7 +-- .../soundsTutorial_constants.py | 4 +- src/extra/SpellChecker/__init__.py | 5 +- src/extra/SpellChecker/spellchecker.py | 7 +-- src/extra/autocompletionUsers/__init__.py | 4 +- src/extra/autocompletionUsers/completion.py | 5 +- src/extra/autocompletionUsers/manage.py | 6 ++- src/extra/autocompletionUsers/settings.py | 8 +-- src/extra/ocr/__init__.py | 4 +- src/extra/translator/__init__.py | 5 +- src/extra/translator/wx_ui.py | 21 +++++++- src/fixes/__init__.py | 9 ++-- src/keys/__init__.py | 1 + src/keystrokeEditor/__init__.py | 3 +- src/keystrokeEditor/keystrokeEditor.py | 5 +- src/keystrokeEditor/wx_ui.py | 2 +- src/multiplatform_widgets/__init__.py | 3 +- src/notifier/__init__.py | 5 +- src/sessionmanager/sessionManager.py | 9 ++-- src/sessions/base.py | 3 +- src/sessions/twitter/compose.py | 33 ++++++------ src/sessions/twitter/long_tweets/tweets.py | 10 ++-- src/sessions/twitter/long_tweets/twishort.py | 5 +- src/sessions/twitter/session.py | 51 +++++++++--------- src/sessions/twitter/utils.py | 53 ++++++++++--------- src/update/updater.py | 5 +- src/update/wxUpdater.py | 3 +- src/widgetUtils/__init__.py | 3 +- src/wxUI/buffers/__init__.py | 21 ++++---- src/wxUI/buffers/dm.py | 3 +- src/wxUI/buffers/favourites.py | 3 +- src/wxUI/buffers/lists.py | 3 +- src/wxUI/buffers/people.py | 3 +- src/wxUI/buffers/tweet_searches.py | 3 +- src/wxUI/buffers/user_searches.py | 3 +- src/wxUI/dialogs/__init__.py | 3 +- src/wxUI/dialogs/configuration.py | 3 +- src/wxUI/dialogs/filterDialogs.py | 4 +- src/wxUI/dialogs/find.py | 4 +- src/wxUI/dialogs/search.py | 3 +- src/wxUI/dialogs/show_user.py | 3 +- src/wxUI/dialogs/trends.py | 4 +- src/wxUI/dialogs/update_profile.py | 3 +- src/wxUI/dialogs/utils.py | 3 +- 53 files changed, 264 insertions(+), 190 deletions(-) diff --git a/src/controller/buffers/twitterBuffers.py b/src/controller/buffers/twitterBuffers.py index 5b9701c0..4450d996 100644 --- a/src/controller/buffers/twitterBuffers.py +++ b/src/controller/buffers/twitterBuffers.py @@ -128,7 +128,7 @@ class baseBufferController(baseBuffers.buffer): tweetsList = [] tweet_id = tweet["id"] message = None - if tweet.has_key("message"): + if "message" in tweet: message = tweet["message"] try: tweet = self.session.twitter.show_status(id=tweet_id, include_ext_alt_text=True, tweet_mode="extended") @@ -241,7 +241,7 @@ class baseBufferController(baseBuffers.buffer): if self.name[:-9] in self.session.settings["other_buffers"]["timelines"]: self.session.settings["other_buffers"]["timelines"].remove(self.name[:-9]) self.session.settings.write() - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) return True elif dlg == widgetUtils.NO: @@ -254,7 +254,7 @@ class baseBufferController(baseBuffers.buffer): if dlg == widgetUtils.YES: if self.name[:-9] in self.session.settings["other_buffers"]["favourites_timelines"]: self.session.settings["other_buffers"]["favourites_timelines"].remove(self.name[:-9]) - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) self.session.settings.write() return True @@ -377,7 +377,7 @@ class baseBufferController(baseBuffers.buffer): self.show_menu(widgetUtils.MENU, pos=self.buffer.list.list.GetPosition()) def get_tweet(self): - if self.session.db[self.name][self.buffer.list.get_selected()].has_key("retweeted_status"): + if "retweeted_status" in self.session.db[self.name][self.buffer.list.get_selected()]: tweet = self.session.db[self.name][self.buffer.list.get_selected()]["retweeted_status"] else: tweet = self.session.db[self.name][self.buffer.list.get_selected()] @@ -392,7 +392,7 @@ class baseBufferController(baseBuffers.buffer): tweet = self.get_right_tweet() screen_name = tweet["user"]["screen_name"] id = tweet["id"] - twishort_enabled = tweet.has_key("twishort") + twishort_enabled = "twishort" in tweet users = utils.get_all_mentioned(tweet, self.session.db, field="screen_name") ids = utils.get_all_mentioned(tweet, self.session.db, field="id_str") # Build the window title @@ -489,9 +489,9 @@ class baseBufferController(baseBuffers.buffer): def _retweet_with_comment(self, tweet, id, comment=''): # If quoting a retweet, let's quote the original tweet instead the retweet. - if tweet.has_key("retweeted_status"): + if "retweeted_status" in tweet: tweet = tweet["retweeted_status"] - if tweet.has_key("full_text"): + if "full_text" in tweet: comments = tweet["full_text"] else: comments = tweet["text"] @@ -723,7 +723,7 @@ class sentDirectMessagesController(directMessagesController): def __init__(self, *args, **kwargs): super(sentDirectMessagesController, self).__init__(*args, **kwargs) - if self.session.db.has_key("sent_direct_messages") == False: + if ("sent_direct_messages" in self.session.db) == False: self.session.db["sent_direct_messages"] = {"items": []} def get_more_items(self): @@ -770,7 +770,7 @@ class listBufferController(baseBufferController): if dlg == widgetUtils.YES: if self.name[:-5] in self.session.settings["other_buffers"]["lists"]: self.session.settings["other_buffers"]["lists"].remove(self.name[:-5]) - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) self.session.settings.write() return True @@ -805,7 +805,7 @@ class peopleBufferController(baseBufferController): if dlg == widgetUtils.YES: if self.name[:-10] in self.session.settings["other_buffers"]["followers_timelines"]: self.session.settings["other_buffers"]["followers_timelines"].remove(self.name[:-10]) - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) self.session.settings.write() return True @@ -819,7 +819,7 @@ class peopleBufferController(baseBufferController): if dlg == widgetUtils.YES: if self.name[:-8] in self.session.settings["other_buffers"]["friends_timelines"]: self.session.settings["other_buffers"]["friends_timelines"].remove(self.name[:-8]) - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) self.session.settings.write() return True @@ -1000,7 +1000,7 @@ class searchBufferController(baseBufferController): if self.name[:-11] in self.session.settings["other_buffers"]["tweet_searches"]: self.session.settings["other_buffers"]["tweet_searches"].remove(self.name[:-11]) self.session.settings.write() - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) return True elif dlg == widgetUtils.NO: @@ -1051,7 +1051,7 @@ class searchPeopleBufferController(peopleBufferController): self.args = args self.kwargs = kwargs self.function = function - if self.kwargs.has_key("page") == False: + if ("page" in self.kwargs) == False: self.kwargs["page"] = 1 def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=True): @@ -1115,7 +1115,7 @@ class searchPeopleBufferController(peopleBufferController): if self.name[:-11] in self.session.settings["other_buffers"]["tweet_searches"]: self.session.settings["other_buffers"]["tweet_searches"].remove(self.name[:-11]) self.session.settings.write() - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) return True elif dlg == widgetUtils.NO: @@ -1188,7 +1188,7 @@ class trendsBufferController(baseBuffers.buffer): if self.name[:-3] in self.session.settings["other_buffers"]["trending_topic_buffers"]: self.session.settings["other_buffers"]["trending_topic_buffers"].remove(self.name[:-3]) self.session.settings.write() - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) return True elif dlg == widgetUtils.NO: @@ -1288,7 +1288,7 @@ class conversationBufferController(searchBufferController): else: dlg = widgetUtils.YES if dlg == widgetUtils.YES: - if self.session.db.has_key(self.name): + if self.name in self.session.db: self.session.db.pop(self.name) return True elif dlg == widgetUtils.NO: diff --git a/src/controller/filterController.py b/src/controller/filterController.py index 1ed1487a..1ece0bf9 100644 --- a/src/controller/filterController.py +++ b/src/controller/filterController.py @@ -30,7 +30,7 @@ class filter(object): if i["name"] in langs: langcodes.append(i["code"]) d = dict(in_buffer=self.buffer.name, word=term, regexp=regexp, in_lang=lang_option, languages=langcodes, if_word_exists=contains, allow_rts=allow_rts, allow_quotes=allow_quotes, allow_replies=allow_replies) - if self.buffer.session.settings["filters"].has_key(title): + if title in self.buffer.session.settings["filters"]: return commonMessageDialogs.existing_filter() self.buffer.session.settings["filters"][title] = d self.buffer.session.settings.write() diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 74728c43..11b6b27e 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import platform system = platform.system() import application @@ -8,15 +9,15 @@ import arrow if system == "Windows": from update import updater from wxUI import (view, dialogs, commonMessageDialogs, sysTrayIcon) - import settings + from . import settings from extra import SoundsTutorial, ocr import keystrokeEditor from keyboard_handler.wx_handler import WXKeyboardHandler - import userActionsController - import trendingTopics - import user - import listsController - import filterController + from . import userActionsController + from . import trendingTopics + from . import user + from . import listsController + from . import filterController # from issueReporter import issueReporter elif system == "Linux": from gtkUI import (view, commonMessageDialogs) @@ -24,7 +25,7 @@ from sessions.twitter import utils, compose from sessionmanager import manager, sessionManager from controller.buffers import baseBuffers, twitterBuffers -import messages +from . import messages import sessions from sessions.twitter import session as session_ from pubsub import pub @@ -392,7 +393,7 @@ class Controller(object): def set_buffer_positions(self, session): "Sets positions for buffers if values exist in the database." for i in self.buffers: - if i.account == session.db["user_name"] and session.db.has_key(i.name+"_pos") and hasattr(i.buffer,'list'): + if i.account == session.db["user_name"] and i.name+"_pos" in session.db and hasattr(i.buffer,'list'): i.buffer.list.select_item(session.db[str(i.name+"_pos")]) def logout_account(self, session_id): @@ -1563,7 +1564,7 @@ class Controller(object): output.speak(_(u"Invalid buffer")) return tweet = buffer.get_tweet() - if tweet.has_key("entities") == False or tweet["entities"].has_key("media") == False: + if ("entities" in tweet) == False or ("media" in tweet["entities"]) == False: output.speak(_(u"This tweet doesn't contain images")) return if len(tweet["entities"]["media"]) > 1: diff --git a/src/controller/messages.py b/src/controller/messages.py index d423764f..8668e6cd 100644 --- a/src/controller/messages.py +++ b/src/controller/messages.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import re import platform -import attach +from . import attach import arrow import languageHandler system = platform.system() @@ -206,25 +207,25 @@ class viewTweet(basicTweet): text = "" for i in xrange(0, len(tweetList)): # tweets with message keys are longer tweets, the message value is the full messaje taken from twishort. - if tweetList[i].has_key("message") and tweetList[i]["is_quote_status"] == False: + if "message" in tweetList[i] and tweetList[i]["is_quote_status"] == False: value = "message" else: value = "full_text" - if tweetList[i].has_key("retweeted_status") and tweetList[i]["is_quote_status"] == False: - if tweetList[i].has_key("message") == False: + if "retweeted_status" in tweetList[i] and tweetList[i]["is_quote_status"] == False: + if ("message" in tweetList[i]) == False: text = text + "rt @%s: %s\n" % (tweetList[i]["retweeted_status"]["user"]["screen_name"], tweetList[i]["retweeted_status"]["full_text"]) else: text = text + "rt @%s: %s\n" % (tweetList[i]["retweeted_status"]["user"]["screen_name"], tweetList[i][value]) else: text = text + " @%s: %s\n" % (tweetList[i]["user"]["screen_name"], tweetList[i][value]) # tweets with extended_entities could include image descriptions. - if tweetList[i].has_key("extended_entities") and tweetList[i]["extended_entities"].has_key("media"): + if "extended_entities" in tweetList[i] and "media" in tweetList[i]["extended_entities"]: for z in tweetList[i]["extended_entities"]["media"]: - if z.has_key("ext_alt_text") and z["ext_alt_text"] != None: + if "ext_alt_text" in z and z["ext_alt_text"] != None: image_description.append(z["ext_alt_text"]) - if tweetList[i].has_key("retweeted_status") and tweetList[i]["retweeted_status"].has_key("extended_entities") and tweetList[i]["retweeted_status"]["extended_entities"].has_key("media"): + if "retweeted_status" in tweetList[i] and "extended_entities" in tweetList[i]["retweeted_status"] and "media" in tweetList[i]["retweeted_status"]["extended_entities"]: for z in tweetList[i]["retweeted_status"]["extended_entities"]["media"]: - if z.has_key("ext_alt_text") and z["ext_alt_text"] != None: + if "ext_alt_text" in z and z["ext_alt_text"] != None: image_description.append(z["ext_alt_text"]) # set rt and likes counters. rt_count = str(tweet["retweet_count"]) @@ -234,25 +235,25 @@ class viewTweet(basicTweet): original_date = arrow.get(tweet["created_at"], "ddd MMM DD H:m:s Z YYYY", locale="en") date = original_date.replace(seconds=utc_offset).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage()) if text == "": - if tweet.has_key("message"): + if "message" in tweet: value = "message" else: value = "full_text" - if tweet.has_key("retweeted_status"): - if tweet.has_key("message") == False: + if "retweeted_status" in tweet: + if ("message" in tweet) == False: text = "rt @%s: %s" % (tweet["retweeted_status"]["user"]["screen_name"], tweet["retweeted_status"]["full_text"]) else: text = "rt @%s: %s" % (tweet["retweeted_status"]["user"]["screen_name"], tweet[value]) else: text = tweet[value] text = self.clear_text(text) - if tweet.has_key("extended_entities") and tweet["extended_entities"].has_key("media"): + if "extended_entities" in tweet and "media" in tweet["extended_entities"]: for z in tweet["extended_entities"]["media"]: - if z.has_key("ext_alt_text") and z["ext_alt_text"] != None: + if "ext_alt_text" in z and z["ext_alt_text"] != None: image_description.append(z["ext_alt_text"]) - if tweet.has_key("retweeted_status") and tweet["retweeted_status"].has_key("extended_entities") and tweet["retweeted_status"]["extended_entities"].has_key("media"): + if "retweeted_status" in tweet and "extended_entities" in tweet["retweeted_status"] and "media" in tweet["retweeted_status"]["extended_entities"]: for z in tweet["retweeted_status"]["extended_entities"]["media"]: - if z.has_key("ext_alt_text") and z["ext_alt_text"] != None: + if "ext_alt_text" in z and z["ext_alt_text"] != None: image_description.append(z["ext_alt_text"]) self.message = message.viewTweet(text, rt_count, favs_count, source.decode("utf-8"), date) self.message.set_title(len(text)) diff --git a/src/controller/userActionsController.py b/src/controller/userActionsController.py index 1d0cc788..f3f40ded 100644 --- a/src/controller/userActionsController.py +++ b/src/controller/userActionsController.py @@ -71,7 +71,7 @@ class userActionsController(object): def ignore_client(self, user): tweet = self.buffer.get_right_tweet() - if tweet.has_key("sender"): + if "sender" in tweet: output.speak(_(u"You can't ignore direct messages")) return client = re.sub(r"(?s)<.*?>", "", tweet["source"]) diff --git a/src/extra/AudioUploader/audioUploader.py b/src/extra/AudioUploader/audioUploader.py index afc0fee1..824c64c4 100644 --- a/src/extra/AudioUploader/audioUploader.py +++ b/src/extra/AudioUploader/audioUploader.py @@ -16,10 +16,11 @@ # along with this program. If not, see . # ############################################################ +from __future__ import absolute_import import widgetUtils -import wx_ui -import wx_transfer_dialogs -import transfer +from . import wx_ui +from . import wx_transfer_dialogs +from . import transfer import output import tempfile import sound diff --git a/src/extra/AudioUploader/transfer.py b/src/extra/AudioUploader/transfer.py index c8b9827b..d5671977 100644 --- a/src/extra/AudioUploader/transfer.py +++ b/src/extra/AudioUploader/transfer.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import sys import threading import time import logging -from utils import convert_bytes +from .utils import convert_bytes from pubsub import pub log = logging.getLogger("extra.AudioUploader.transfer") from requests_toolbelt.multipart.encoder import MultipartEncoder, MultipartEncoderMonitor @@ -75,9 +76,9 @@ class Upload(object): data = self.response.json() except: return _("Error in file upload: {0}").format(self.data.content,) - if data.has_key("url") and data["url"] != "0": + if "url" in data and data["url"] != "0": return data["url"] - elif data.has_key("error") and data["error"] != "0": + elif "error" in data and data["error"] != "0": return data["error"] else: return _("Error in file upload: {0}").format(self.data.content,) \ No newline at end of file diff --git a/src/extra/AudioUploader/wx_transfer_dialogs.py b/src/extra/AudioUploader/wx_transfer_dialogs.py index c209ad5e..c3fc01fa 100644 --- a/src/extra/AudioUploader/wx_transfer_dialogs.py +++ b/src/extra/AudioUploader/wx_transfer_dialogs.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -from utils import * +from .utils import * import widgetUtils class UploadDialog(widgetUtils.BaseDialog): diff --git a/src/extra/SoundsTutorial/__init__.py b/src/extra/SoundsTutorial/__init__.py index 1e2e7e17..20d563a6 100644 --- a/src/extra/SoundsTutorial/__init__.py +++ b/src/extra/SoundsTutorial/__init__.py @@ -1 +1,2 @@ -from soundsTutorial import soundsTutorial +from __future__ import absolute_import +from .soundsTutorial import soundsTutorial diff --git a/src/extra/SoundsTutorial/soundsTutorial.py b/src/extra/SoundsTutorial/soundsTutorial.py index 37d9643a..fd835410 100644 --- a/src/extra/SoundsTutorial/soundsTutorial.py +++ b/src/extra/SoundsTutorial/soundsTutorial.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import platform import widgetUtils import os import paths import logging log = logging.getLogger("extra.SoundsTutorial.soundsTutorial") -import soundsTutorial_constants +from . import soundsTutorial_constants if platform.system() == "Windows": - import wx_ui as UI + from . import wx_ui as UI elif platform.system() == "Linux": - import gtk_ui as UI + from . import gtk_ui as UI class soundsTutorial(object): def __init__(self, sessionObject): diff --git a/src/extra/SoundsTutorial/soundsTutorial_constants.py b/src/extra/SoundsTutorial/soundsTutorial_constants.py index ae7cfe63..667e2373 100644 --- a/src/extra/SoundsTutorial/soundsTutorial_constants.py +++ b/src/extra/SoundsTutorial/soundsTutorial_constants.py @@ -1,5 +1,7 @@ #-*- coding: utf-8 -*- -import reverse_sort +from __future__ import absolute_import +#-*- coding: utf-8 -*- +from . import reverse_sort import application actions = reverse_sort.reverse_sort([ ("audio", _(u"Audio tweet.")), ("create_timeline", _(u"User timeline buffer created.")), diff --git a/src/extra/SpellChecker/__init__.py b/src/extra/SpellChecker/__init__.py index fedad8a8..4edff483 100644 --- a/src/extra/SpellChecker/__init__.py +++ b/src/extra/SpellChecker/__init__.py @@ -1,4 +1,5 @@ -import spellchecker +from __future__ import absolute_import +from . import spellchecker import platform if platform.system() == "Windows": - from wx_ui import * \ No newline at end of file + from .wx_ui import * \ No newline at end of file diff --git a/src/extra/SpellChecker/spellchecker.py b/src/extra/SpellChecker/spellchecker.py index a40102b5..cb3ce50d 100644 --- a/src/extra/SpellChecker/spellchecker.py +++ b/src/extra/SpellChecker/spellchecker.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import os import logging -import wx_ui +from . import wx_ui import widgetUtils import output import config import languageHandler import enchant import paths -import twitterFilter +from . import twitterFilter from enchant.checker import SpellChecker from enchant.errors import DictNotFoundError from enchant import tokenize @@ -52,7 +53,7 @@ class spellChecker(object): def check(self): try: - self.checker.next() + next(self.checker) textToSay = _(u"Misspelled word: %s") % (self.checker.word,) context = u"... %s %s %s" % (self.checker.leading_context(10), self.checker.word, self.checker.trailing_context(10)) self.dialog.set_title(textToSay) diff --git a/src/extra/autocompletionUsers/__init__.py b/src/extra/autocompletionUsers/__init__.py index 343ea6c0..888d9511 100644 --- a/src/extra/autocompletionUsers/__init__.py +++ b/src/extra/autocompletionUsers/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- -import completion, settings \ No newline at end of file +from __future__ import absolute_import +# -*- coding: utf-8 -*- +from . import completion, settings \ No newline at end of file diff --git a/src/extra/autocompletionUsers/completion.py b/src/extra/autocompletionUsers/completion.py index f9c9a516..7dedc525 100644 --- a/src/extra/autocompletionUsers/completion.py +++ b/src/extra/autocompletionUsers/completion.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import output -import storage -import wx_menu +from . import storage +from . import wx_menu class autocompletionUsers(object): def __init__(self, window, session_id): diff --git a/src/extra/autocompletionUsers/manage.py b/src/extra/autocompletionUsers/manage.py index 60e34ddc..a42ef607 100644 --- a/src/extra/autocompletionUsers/manage.py +++ b/src/extra/autocompletionUsers/manage.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- -import storage +from __future__ import absolute_import +# -*- coding: utf-8 -*- +from . import storage import widgetUtils -import wx_manage +from . import wx_manage from wxUI import commonMessageDialogs class autocompletionManage(object): diff --git a/src/extra/autocompletionUsers/settings.py b/src/extra/autocompletionUsers/settings.py index 6eb0762d..5b682850 100644 --- a/src/extra/autocompletionUsers/settings.py +++ b/src/extra/autocompletionUsers/settings.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- -import storage +from __future__ import absolute_import +# -*- coding: utf-8 -*- +from . import storage import widgetUtils -import wx_settings -import manage +from . import wx_settings +from . import manage import output from mysc.thread_utils import call_threaded diff --git a/src/extra/ocr/__init__.py b/src/extra/ocr/__init__.py index d127ddeb..b39f07d2 100644 --- a/src/extra/ocr/__init__.py +++ b/src/extra/ocr/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- -import OCRSpace \ No newline at end of file +from __future__ import absolute_import +# -*- coding: utf-8 -*- +from . import OCRSpace \ No newline at end of file diff --git a/src/extra/translator/__init__.py b/src/extra/translator/__init__.py index be356d51..9296eede 100644 --- a/src/extra/translator/__init__.py +++ b/src/extra/translator/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -import translator +from __future__ import absolute_import +from . import translator import platform if platform.system() == "Windows": - import wx_ui as gui + from . import wx_ui as gui \ No newline at end of file diff --git a/src/extra/translator/wx_ui.py b/src/extra/translator/wx_ui.py index a61c9e7e..43b1c684 100644 --- a/src/extra/translator/wx_ui.py +++ b/src/extra/translator/wx_ui.py @@ -16,7 +16,26 @@ # along with this program. If not, see . # ############################################################ -import translator +from __future__ import absolute_import +# -*- coding: utf-8 -*- +############################################################ +# Copyright (c) 2013, 2014 Manuel Eduardo Cortéz Vallejo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################ +from . import translator import wx from wxUI.dialogs import baseDialog diff --git a/src/fixes/__init__.py b/src/fixes/__init__.py index 354a5875..c14e4d6d 100644 --- a/src/fixes/__init__.py +++ b/src/fixes/__init__.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- """ This module contains some bugfixes for packages used in TWBlue.""" +from __future__ import absolute_import import sys -import fix_arrow # A few new locales for Three languages in arrow. -import fix_urllib3_warnings # Avoiding some SSL warnings related to Twython. -import fix_win32com -import fix_requests #fix cacert.pem location for TWBlue binary copies +from . import fix_arrow # A few new locales for Three languages in arrow. +from . import fix_urllib3_warnings # Avoiding some SSL warnings related to Twython. +from . import fix_win32com +from . import fix_requests #fix cacert.pem location for TWBlue binary copies def setup(): fix_arrow.fix() if hasattr(sys, "frozen"): diff --git a/src/keys/__init__.py b/src/keys/__init__.py index 69818a83..af724223 100644 --- a/src/keys/__init__.py +++ b/src/keys/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from builtins import object import application import platform import exceptions diff --git a/src/keystrokeEditor/__init__.py b/src/keystrokeEditor/__init__.py index aef2cc0f..c76ba54e 100644 --- a/src/keystrokeEditor/__init__.py +++ b/src/keystrokeEditor/__init__.py @@ -1 +1,2 @@ -from keystrokeEditor import KeystrokeEditor \ No newline at end of file +from __future__ import absolute_import +from .keystrokeEditor import KeystrokeEditor \ No newline at end of file diff --git a/src/keystrokeEditor/keystrokeEditor.py b/src/keystrokeEditor/keystrokeEditor.py index 3f47abbc..c56b4cd0 100644 --- a/src/keystrokeEditor/keystrokeEditor.py +++ b/src/keystrokeEditor/keystrokeEditor.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import widgetUtils import config -import wx_ui -import constants +from . import wx_ui +from . import constants from pubsub import pub class KeystrokeEditor(object): diff --git a/src/keystrokeEditor/wx_ui.py b/src/keystrokeEditor/wx_ui.py index c8162ffa..dbd922d1 100644 --- a/src/keystrokeEditor/wx_ui.py +++ b/src/keystrokeEditor/wx_ui.py @@ -32,7 +32,7 @@ class keystrokeEditorDialog(baseDialog.BaseWXDialog): selection = self.keys.get_selected() self.keys.clear() for i in keystrokes: - if actions.has_key(i) == False: + if (i in actions) == False: continue action = actions[i] self.actions.append(i) diff --git a/src/multiplatform_widgets/__init__.py b/src/multiplatform_widgets/__init__.py index 126802d0..b282b4e9 100644 --- a/src/multiplatform_widgets/__init__.py +++ b/src/multiplatform_widgets/__init__.py @@ -1 +1,2 @@ -import widgets \ No newline at end of file +from __future__ import absolute_import +from . import widgets \ No newline at end of file diff --git a/src/notifier/__init__.py b/src/notifier/__init__.py index 3bd1ff6e..5eaab0ec 100644 --- a/src/notifier/__init__.py +++ b/src/notifier/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """ A cross platform notification system. Under Linux, the wx.NotificationMessage does not show a notification on the taskbar, so we decided to use dbus for showing notifications for linux and wx for Windows.""" +from __future__ import absolute_import import platform notify = None @@ -8,10 +9,10 @@ notify = None def setup(): global notify if platform.system() == "Windows": - import windows + from . import windows notify = windows.notification() elif platform.system() == "Linux": - import linux + from . import linux notify = linux.notification() def send(title, text): diff --git a/src/sessionmanager/sessionManager.py b/src/sessionmanager/sessionManager.py index caf72820..a3cacd5c 100644 --- a/src/sessionmanager/sessionManager.py +++ b/src/sessionmanager/sessionManager.py @@ -1,20 +1,21 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import shutil import widgetUtils import platform import output if platform.system() == "Windows": - import wxUI as view + from . import wxUI as view from controller import settings elif platform.system() == "Linux": - import gtkUI as view + from . import gtkUI as view import paths import time import os import logging import sessions from sessions.twitter import session -import manager +from . import manager import config_utils import config @@ -77,7 +78,7 @@ class sessionManagerController(object): def do_ok(self): log.debug("Starting sessions...") for i in self.sessions: - if sessions.sessions.has_key(i) == True: continue + if (i in sessions.sessions) == True: continue s = session.Session(i) s.get_configuration() if i not in config.app["sessions"]["ignored_sessions"]: diff --git a/src/sessions/base.py b/src/sessions/base.py index 39f635a7..418f5875 100644 --- a/src/sessions/base.py +++ b/src/sessions/base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """ A base class to be derived in possible new sessions for TWBlue and services.""" +from __future__ import absolute_import import paths import output import time @@ -9,7 +10,7 @@ import config_utils import shelve import application import os -import session_exceptions as Exceptions +from . import session_exceptions as Exceptions log = logging.getLogger("sessionmanager.session") class baseSession(object): diff --git a/src/sessions/twitter/compose.py b/src/sessions/twitter/compose.py index 79138ee3..554f95de 100644 --- a/src/sessions/twitter/compose.py +++ b/src/sessions/twitter/compose.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import platform system = platform.system() -import utils +from . import utils import re import htmlentitydefs import time @@ -10,7 +11,7 @@ import languageHandler import arrow import logging import config -from long_tweets import twishort, tweets +from .long_tweets import twishort, tweets log = logging.getLogger("compose") def StripChars(s): @@ -38,13 +39,13 @@ def compose_tweet(tweet, db, relative_times, show_screen_names=False, session=No ts = original_date.replace(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.getLanguage()) else: ts = tweet["created_at"] - if tweet.has_key("message"): + if "message" in tweet: value = "message" - elif tweet.has_key("full_text"): + elif "full_text" in tweet: value = "full_text" else: value = "text" - if tweet.has_key("retweeted_status") and value != "message": + if "retweeted_status" in tweet and value != "message": text = StripChars(tweet["retweeted_status"][value]) else: text = StripChars(tweet[value]) @@ -53,16 +54,16 @@ def compose_tweet(tweet, db, relative_times, show_screen_names=False, session=No else: user = tweet["user"]["name"] source = re.sub(r"(?s)<.*?>", "", tweet["source"]) - if tweet.has_key("retweeted_status"): - if tweet.has_key("message") == False and tweet["retweeted_status"]["is_quote_status"] == False: + if "retweeted_status" in tweet: + if ("message" in tweet) == False and tweet["retweeted_status"]["is_quote_status"] == False: text = "RT @%s: %s" % (tweet["retweeted_status"]["user"]["screen_name"], text) elif tweet["retweeted_status"]["is_quote_status"]: text = "%s" % (text) else: text = "RT @%s: %s" % (tweet["retweeted_status"]["user"]["screen_name"], text) - if tweet.has_key("message") == False: + if ("message" in tweet) == False: urls = utils.find_urls_in_text(text) - if tweet.has_key("retweeted_status"): + if "retweeted_status" in tweet: for url in range(0, len(urls)): try: text = text.replace(urls[url], tweet["retweeted_status"]["entities"]["urls"][url]["expanded_url"]) @@ -110,14 +111,14 @@ def compose_direct_message(item, db, relative_times, show_screen_names=False, se def compose_quoted_tweet(quoted_tweet, original_tweet, show_screen_names=False, session=None): """ It receives a tweet and returns a list with the user, text for the tweet or message, date and the client where user is.""" - if quoted_tweet.has_key("retweeted_status"): - if quoted_tweet["retweeted_status"].has_key("full_text"): + if "retweeted_status" in quoted_tweet: + if "full_text" in quoted_tweet["retweeted_status"]: value = "full_text" else: value = "text" text = StripChars(quoted_tweet["retweeted_status"][value]) else: - if quoted_tweet.has_key("full_text"): + if "full_text" in quoted_tweet: value = "full_text" else: value = "text" @@ -127,13 +128,13 @@ def compose_quoted_tweet(quoted_tweet, original_tweet, show_screen_names=False, else: quoting_user = quoted_tweet["user"]["name"] source = re.sub(r"(?s)<.*?>", "", quoted_tweet["source"]) - if quoted_tweet.has_key("retweeted_status"): + if "retweeted_status" in quoted_tweet: text = "rt @%s: %s" % (quoted_tweet["retweeted_status"]["user"]["screen_name"], text) if text[-1] in chars: text=text+"." original_user = original_tweet["user"]["screen_name"] - if original_tweet.has_key("message"): + if "message" in original_tweet: original_text = original_tweet["message"] - elif original_tweet.has_key("full_text"): + elif "full_text" in original_tweet: original_text = StripChars(original_tweet["full_text"]) else: original_text = StripChars(original_tweet["text"]) @@ -151,7 +152,7 @@ def compose_followers_list(tweet, db, relative_times=True, show_screen_names=Fal ts = original_date.replace(seconds=db["utc_offset"]).format(_(u"dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.getLanguage()) else: ts = tweet["created_at"] - if tweet.has_key("status"): + if "status" in tweet: if len(tweet["status"]) > 4 and system == "Windows": original_date2 = arrow.get(tweet["status"]["created_at"], "ddd MMM D H:m:s Z YYYY", locale="en") if relative_times: diff --git a/src/sessions/twitter/long_tweets/tweets.py b/src/sessions/twitter/long_tweets/tweets.py index 95e6052e..5445f8fa 100644 --- a/src/sessions/twitter/long_tweets/tweets.py +++ b/src/sessions/twitter/long_tweets/tweets.py @@ -22,9 +22,9 @@ def is_long(tweet): """ Check if the passed tweet contains a quote in its metadata. tweet dict: a tweet dictionary. returns True if a quote is detected, False otherwise.""" - if tweet.has_key("quoted_status_id") and tweet.has_key("quoted_status"): + if "quoted_status_id" in tweet and "quoted_status" in tweet: return tweet["quoted_status_id"] - elif tweet.has_key("retweeted_status") and tweet["retweeted_status"].has_key("quoted_status_id") and tweet["retweeted_status"].has_key("quoted_status"): + elif "retweeted_status" in tweet and "quoted_status_id" in tweet["retweeted_status"] and "quoted_status" in tweet["retweeted_status"]: return tweet["retweeted_status"]["quoted_status_id"] return False @@ -32,8 +32,8 @@ def clear_url(tweet): """ Reads data from a quoted tweet and removes the link to the Status from the tweet's text. tweet dict: a tweet dictionary. returns a tweet dictionary without the URL to the status ID in its text to display.""" - if tweet.has_key("retweeted_status"): - if tweet["retweeted_status"].has_key("full_text"): + if "retweeted_status" in tweet: + if "full_text" in tweet["retweeted_status"]: value = "full_text" else: value = "text" @@ -41,7 +41,7 @@ def clear_url(tweet): try: tweet["message"] = tweet["message"].replace(urls[-1], "") except IndexError: pass else: - if tweet.has_key("full_text"): + if "full_text" in tweet: value = "full_text" else: value = "text" diff --git a/src/sessions/twitter/long_tweets/twishort.py b/src/sessions/twitter/long_tweets/twishort.py index df27ccbc..46481828 100644 --- a/src/sessions/twitter/long_tweets/twishort.py +++ b/src/sessions/twitter/long_tweets/twishort.py @@ -16,6 +16,7 @@ # along with this program. If not, see . # ############################################################ +from __future__ import print_function import logging import requests import keys @@ -47,7 +48,7 @@ def is_long(tweet): # see https://github.com/manuelcortez/TWBlue/issues/103 except TypeError: pass - if long == False and tweet.has_key("retweeted_status"): + if long == False and "retweeted_status" in tweet: for url in range(0, len(tweet["retweeted_status"]["entities"]["urls"])): try: if tweet["retweeted_status"]["entities"]["urls"][url] != None and "twishort.com" in tweet["retweeted_status"]["entities"]["urls"][url]["expanded_url"]: @@ -97,5 +98,5 @@ def create_tweet(user_token, user_secret, text, media=0): try: return response.json()["text_to_tweet"] except: - print "There was a problem creating a long tweet" + print("There was a problem creating a long tweet") return 0 \ No newline at end of file diff --git a/src/sessions/twitter/session.py b/src/sessions/twitter/session.py index 1a08198e..fbcd429d 100644 --- a/src/sessions/twitter/session.py +++ b/src/sessions/twitter/session.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """ This is the main session needed to access all Twitter Features.""" +from __future__ import absolute_import import os import time import logging @@ -15,7 +16,7 @@ from keys import keyring from sessions import base from sessions.twitter import utils, compose from sessions.twitter.long_tweets import tweets, twishort -from wxUI import authorisationDialog +from .wxUI import authorisationDialog log = logging.getLogger("sessions.twitterSession") @@ -30,9 +31,9 @@ class Session(base.baseSession): returns the number of items that have been added in this execution""" num = 0 last_id = None - if self.db.has_key(name) == False: + if (name in self.db) == False: self.db[name] = [] - if self.db.has_key("users") == False: + if ("users" in self.db) == False: self.db["users"] = {} if ignore_older and len(self.db[name]) > 0: if self.settings["general"]["reverse_timelines"] == False: @@ -51,8 +52,8 @@ class Session(base.baseSession): if self.settings["general"]["reverse_timelines"] == False: self.db[name].append(i) else: self.db[name].insert(0, i) num = num+1 - if i.has_key("user") == True: - if self.db["users"].has_key(i["user"]["id"]) == False: + if ("user" in i) == True: + if (i["user"]["id"] in self.db["users"]) == False: self.db["users"][i["user"]["id"]] = i["user"] return num @@ -66,7 +67,7 @@ class Session(base.baseSession): if name == "direct_messages": return self.order_direct_messages(data) num = 0 - if self.db.has_key(name) == False: + if (name in self.db) == False: self.db[name] = {} self.db[name]["items"] = [] for i in data: @@ -82,12 +83,12 @@ class Session(base.baseSession): returns the number of incoming messages processed in this execution, and sends an event with data regarding amount of sent direct messages added.""" incoming = 0 sent = 0 - if self.db.has_key("direct_messages") == False: + if ("direct_messages" in self.db) == False: self.db["direct_messages"] = {} self.db["direct_messages"]["items"] = [] for i in data: if i["message_create"]["sender_id"] == self.db["user_id"]: - if self.db.has_key("sent_direct_messages") and utils.find_item(i["id"], self.db["sent_direct_messages"]["items"]) == None: + if "sent_direct_messages" in self.db and utils.find_item(i["id"], self.db["sent_direct_messages"]["items"]) == None: if self.settings["general"]["reverse_timelines"] == False: self.db["sent_direct_messages"]["items"].append(i) else: self.db["sent_direct_messages"]["items"].insert(0, i) sent = sent+1 @@ -164,13 +165,13 @@ class Session(base.baseSession): users, dm bool: If any of these is set to True, the function will treat items as users or dm (they need different handling). name str: name of the database item to put new element in.""" results = [] - if kwargs.has_key("cursor") and kwargs["cursor"] == 0: + if "cursor" in kwargs and kwargs["cursor"] == 0: output.speak(_(u"There are no more items to retrieve in this buffer.")) return data = getattr(self.twitter, update_function)(*args, **kwargs) if users == True: - if type(data) == dict and data.has_key("next_cursor"): - if data.has_key("next_cursor"): # There are more objects to retrieve. + if type(data) == dict and "next_cursor" in data: + if "next_cursor" in data: # There are more objects to retrieve. self.db[name]["cursor"] = data["next_cursor"] else: # Set cursor to 0, wich means no more items available. self.db[name]["cursor"] = 0 @@ -178,7 +179,7 @@ class Session(base.baseSession): elif type(data) == list: results.extend(data[1:]) elif dm == True: - if data.has_key("next_cursor"): # There are more objects to retrieve. + if "next_cursor" in data: # There are more objects to retrieve. self.db[name]["cursor"] = data["next_cursor"] else: # Set cursor to 0, wich means no more items available. self.db[name]["cursor"] = 0 @@ -289,7 +290,7 @@ class Session(base.baseSession): name str: Name to save items to the database. function str: A function to get the items.""" last_id = -1 - if self.db.has_key(name): + if name in self.db: try: if self.db[name][0]["id"] > self.db[name][-1]["id"]: last_id = self.db[name][0]["id"] @@ -309,7 +310,7 @@ class Session(base.baseSession): returns number of items retrieved.""" items_ = [] try: - if self.db[name].has_key("cursor") and get_previous: + if "cursor" in self.db[name] and get_previous: cursor = self.db[name]["cursor"] else: cursor = -1 @@ -322,7 +323,7 @@ class Session(base.baseSession): tl[items].reverse() num = self.order_cursored_buffer(name, tl[items]) # Recently, Twitter's new endpoints have cursor if there are more results. - if tl.has_key("next_cursor"): + if "next_cursor" in tl: self.db[name]["cursor"] = tl["next_cursor"] else: self.db[name]["cursor"] = 0 @@ -352,7 +353,7 @@ class Session(base.baseSession): def get_quoted_tweet(self, tweet): """ Process a tweet and extract all information related to the quote.""" quoted_tweet = tweet - if tweet.has_key("full_text"): + if "full_text" in tweet: value = "full_text" else: value = "text" @@ -360,16 +361,16 @@ class Session(base.baseSession): for url in range(0, len(urls)): try: quoted_tweet[value] = quoted_tweet[value].replace(urls[url], quoted_tweet["entities"]["urls"][url]["expanded_url"]) except IndexError: pass - if quoted_tweet.has_key("quoted_status"): + if "quoted_status" in quoted_tweet: original_tweet = quoted_tweet["quoted_status"] - elif quoted_tweet.has_key("retweeted_status") and quoted_tweet["retweeted_status"].has_key("quoted_status"): + elif "retweeted_status" in quoted_tweet and "quoted_status" in quoted_tweet["retweeted_status"]: original_tweet = quoted_tweet["retweeted_status"]["quoted_status"] else: return quoted_tweet original_tweet = self.check_long_tweet(original_tweet) - if original_tweet.has_key("full_text"): + if "full_text" in original_tweet: value = "full_text" - elif original_tweet.has_key("message"): + elif "message" in original_tweet: value = "message" else: value = "text" @@ -386,13 +387,13 @@ class Session(base.baseSession): long = twishort.is_long(tweet) if long != False and config.app["app-settings"]["handle_longtweets"]: message = twishort.get_full_text(long) - if tweet.has_key("quoted_status"): + if "quoted_status" in tweet: tweet["quoted_status"]["message"] = message if tweet["quoted_status"]["message"] == False: return False tweet["quoted_status"]["twishort"] = True for i in tweet["quoted_status"]["entities"]["user_mentions"]: if "@%s" % (i["screen_name"]) not in tweet["quoted_status"]["message"] and i["screen_name"] != tweet["user"]["screen_name"]: - if tweet["quoted_status"].has_key("retweeted_status") and tweet["retweeted_status"]["user"]["screen_name"] == i["screen_name"]: + if "retweeted_status" in tweet["quoted_status"] and tweet["retweeted_status"]["user"]["screen_name"] == i["screen_name"]: continue tweet["quoted_status"]["message"] = u"@%s %s" % (i["screen_name"], tweet["message"]) else: @@ -401,7 +402,7 @@ class Session(base.baseSession): tweet["twishort"] = True for i in tweet["entities"]["user_mentions"]: if "@%s" % (i["screen_name"]) not in tweet["message"] and i["screen_name"] != tweet["user"]["screen_name"]: - if tweet.has_key("retweeted_status") and tweet["retweeted_status"]["user"]["screen_name"] == i["screen_name"]: + if "retweeted_status" in tweet and tweet["retweeted_status"]["user"]["screen_name"] == i["screen_name"]: continue return tweet @@ -409,7 +410,7 @@ class Session(base.baseSession): """ Returns an user object associated with an ID. id str: User identifier, provided by Twitter. returns an user dict.""" - if self.db.has_key("users") == False or self.db["users"].has_key(id) == False: + if ("users" in self.db) == False or (id in self.db["users"]) == False: user = self.twitter.show_user(id=id) self.db["users"][user["id_str"]] = user return user @@ -420,7 +421,7 @@ class Session(base.baseSession): """ Returns an user identifier associated with a screen_name. screen_name str: User name, such as tw_blue2, provided by Twitter. returns an user ID.""" - if self.db.has_key("users") == False: + if ("users" in self.db) == False: user = utils.if_user_exists(self.twitter, screen_name) self.db["users"][user["id_str"]] = user return user["id_str"] diff --git a/src/sessions/twitter/utils.py b/src/sessions/twitter/utils.py index cf1886fa..2c22ced4 100644 --- a/src/sessions/twitter/utils.py +++ b/src/sessions/twitter/utils.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import print_function import url_shortener, re import output from twython import TwythonError @@ -24,32 +25,32 @@ def find_urls_in_text(text): def find_urls (tweet): urls = [] # Let's add URLS from tweet entities. - if tweet.has_key("message_create"): + if "message_create" in tweet: entities = tweet["message_create"]["message_data"]["entities"] else: entities = tweet["entities"] for i in entities["urls"]: if i["expanded_url"] not in urls: urls.append(i["expanded_url"]) - if tweet.has_key("quoted_status"): + if "quoted_status" in tweet: for i in tweet["quoted_status"]["entities"]["urls"]: if i["expanded_url"] not in urls: urls.append(i["expanded_url"]) - if tweet.has_key("retweeted_status"): + if "retweeted_status" in tweet: for i in tweet["retweeted_status"]["entities"]["urls"]: if i["expanded_url"] not in urls: urls.append(i["expanded_url"]) - if tweet["retweeted_status"].has_key("quoted_status"): + if "quoted_status" in tweet["retweeted_status"]: for i in tweet["retweeted_status"]["quoted_status"]["entities"]["urls"]: if i["expanded_url"] not in urls: urls.append(i["expanded_url"]) - if tweet.has_key("message"): + if "message" in tweet: i = "message" - elif tweet.has_key("full_text"): + elif "full_text" in tweet: i = "full_text" else: i = "text" - if tweet.has_key("message_create"): + if "message_create" in tweet: extracted_urls = find_urls_in_text(tweet["message_create"]["message_data"]["text"]) else: extracted_urls = find_urls_in_text(tweet[i]) @@ -82,7 +83,7 @@ def is_audio(tweet): try: if len(find_urls(tweet)) < 1: return False - if tweet.has_key("message_create"): + if "message_create" in tweet: entities = tweet["message_create"]["message_data"]["entities"] else: entities = tweet["entities"] @@ -91,22 +92,22 @@ def is_audio(tweet): if i["text"] == "audio": return True except IndexError: - print tweet["entities"]["hashtags"] + print(tweet["entities"]["hashtags"]) log.exception("Exception while executing is_audio hashtag algorithm") def is_geocoded(tweet): - if tweet.has_key("coordinates") and tweet["coordinates"] != None: + if "coordinates" in tweet and tweet["coordinates"] != None: return True def is_media(tweet): - if tweet.has_key("message_create"): + if "message_create" in tweet: entities = tweet["message_create"]["message_data"]["entities"] else: entities = tweet["entities"] - if entities.has_key("media") == False: + if ("media" in entities) == False: return False for i in entities["media"]: - if i.has_key("type") and i["type"] == "photo": + if "type" in i and i["type"] == "photo": return True return False @@ -121,10 +122,10 @@ def get_all_mentioned(tweet, conf, field="screen_name"): def get_all_users(tweet, conf): string = [] - if tweet.has_key("retweeted_status"): + if "retweeted_status" in tweet: string.append(tweet["user"]["screen_name"]) tweet = tweet["retweeted_status"] - if tweet.has_key("sender"): + if "sender" in tweet: string.append(tweet["sender"]["screen_name"]) else: if tweet["user"]["screen_name"] != conf["user_name"]: @@ -161,16 +162,16 @@ def api_call(parent=None, call_name=None, preexec_message="", success="", succes def is_allowed(tweet, settings, buffer_name): clients = settings["twitter"]["ignored_clients"] - if tweet.has_key("sender"): return True + if "sender" in tweet: return True allowed = True tweet_data = {} - if tweet.has_key("retweeted_status"): + if "retweeted_status" in tweet: tweet_data["retweet"] = True if tweet["in_reply_to_status_id_str"] != None: tweet_data["reply"] = True - if tweet.has_key("quoted_status"): + if "quoted_status" in tweet: tweet_data["quote"] = True - if tweet.has_key("retweeted_status"): tweet = tweet["retweeted_status"] + if "retweeted_status" in tweet: tweet = tweet["retweeted_status"] source = re.sub(r"(?s)<.*?>", "", tweet["source"]) for i in clients: if i.lower() == source.lower(): @@ -178,7 +179,7 @@ def is_allowed(tweet, settings, buffer_name): return filter_tweet(tweet, tweet_data, settings, buffer_name) def filter_tweet(tweet, tweet_data, settings, buffer_name): - if tweet.has_key("full_text"): + if "full_text" in tweet: value = "full_text" else: value = "text" @@ -187,23 +188,23 @@ def filter_tweet(tweet, tweet_data, settings, buffer_name): regexp = settings["filters"][i]["regexp"] word = settings["filters"][i]["word"] # Added if/else for compatibility reasons. - if settings["filters"][i].has_key("allow_rts"): + if "allow_rts" in settings["filters"][i]: allow_rts = settings["filters"][i]["allow_rts"] else: allow_rts = "True" - if settings["filters"][i].has_key("allow_quotes"): + if "allow_quotes" in settings["filters"][i]: allow_quotes = settings["filters"][i]["allow_quotes"] else: allow_quotes = "True" - if settings["filters"][i].has_key("allow_replies"): + if "allow_replies" in settings["filters"][i]: allow_replies = settings["filters"][i]["allow_replies"] else: allow_replies = "True" - if allow_rts == "False" and tweet_data.has_key("retweet"): + if allow_rts == "False" and "retweet" in tweet_data: return False - if allow_quotes == "False" and tweet_data.has_key("quote"): + if allow_quotes == "False" and "quote" in tweet_data: return False - if allow_replies == "False" and tweet_data.has_key("reply"): + if allow_replies == "False" and "reply" in tweet_data: return False if word != "" and settings["filters"][i]["if_word_exists"]: if word in tweet[value]: diff --git a/src/update/updater.py b/src/update/updater.py index 2384f968..5b19da2d 100644 --- a/src/update/updater.py +++ b/src/update/updater.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import application -import update +from . import update import platform import logging import output from requests.exceptions import ConnectionError -from wxUpdater import * +from .wxUpdater import * logger = logging.getLogger("updater") def do_update(endpoint=application.update_url): diff --git a/src/update/wxUpdater.py b/src/update/wxUpdater.py index 761d620a..b3f3f34e 100644 --- a/src/update/wxUpdater.py +++ b/src/update/wxUpdater.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx import application -import utils +from . import utils progress_dialog = None diff --git a/src/widgetUtils/__init__.py b/src/widgetUtils/__init__.py index 30509ef6..0a15dadd 100644 --- a/src/widgetUtils/__init__.py +++ b/src/widgetUtils/__init__.py @@ -1,5 +1,6 @@ +from __future__ import absolute_import import platform if platform.system() == "Windows": - from wxUtils import * + from .wxUtils import * #elif platform.system() == "Linux": # from gtkUtils import * diff --git a/src/wxUI/buffers/__init__.py b/src/wxUI/buffers/__init__.py index 2380f3a9..d62019d7 100644 --- a/src/wxUI/buffers/__init__.py +++ b/src/wxUI/buffers/__init__.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- -from base import basePanel -from dm import dmPanel -from events import eventsPanel -from favourites import favsPanel -from lists import listPanel -from panels import accountPanel, emptyPanel -from people import peoplePanel -from trends import trendsPanel -from tweet_searches import searchPanel -from user_searches import searchUsersPanel \ No newline at end of file +from __future__ import absolute_import +from .base import basePanel +from .dm import dmPanel +from .events import eventsPanel +from .favourites import favsPanel +from .lists import listPanel +from .panels import accountPanel, emptyPanel +from .people import peoplePanel +from .trends import trendsPanel +from .tweet_searches import searchPanel +from .user_searches import searchUsersPanel \ No newline at end of file diff --git a/src/wxUI/buffers/dm.py b/src/wxUI/buffers/dm.py index db95aae0..774433ed 100644 --- a/src/wxUI/buffers/dm.py +++ b/src/wxUI/buffers/dm.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -from base import basePanel +from .base import basePanel class dmPanel(basePanel): def __init__(self, parent, name): diff --git a/src/wxUI/buffers/favourites.py b/src/wxUI/buffers/favourites.py index 4837dd6f..5dd91a05 100644 --- a/src/wxUI/buffers/favourites.py +++ b/src/wxUI/buffers/favourites.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -from base import basePanel +from .base import basePanel class favsPanel(basePanel): def __init__(self, parent, name): diff --git a/src/wxUI/buffers/lists.py b/src/wxUI/buffers/lists.py index 82ca73bc..baf6c95a 100644 --- a/src/wxUI/buffers/lists.py +++ b/src/wxUI/buffers/lists.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -from base import basePanel +from .base import basePanel class listPanel(basePanel): def __init__(self, parent, name): diff --git a/src/wxUI/buffers/people.py b/src/wxUI/buffers/people.py index f887ba4d..47e5cf17 100644 --- a/src/wxUI/buffers/people.py +++ b/src/wxUI/buffers/people.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx from multiplatform_widgets import widgets -from base import basePanel +from .base import basePanel class peoplePanel(basePanel): """ Buffer used to show people.""" diff --git a/src/wxUI/buffers/tweet_searches.py b/src/wxUI/buffers/tweet_searches.py index 28cf974e..ad0c7aa0 100644 --- a/src/wxUI/buffers/tweet_searches.py +++ b/src/wxUI/buffers/tweet_searches.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -from base import basePanel +from .base import basePanel class searchPanel(basePanel): def __init__(self, parent, name): diff --git a/src/wxUI/buffers/user_searches.py b/src/wxUI/buffers/user_searches.py index b3b1415c..c1ff9745 100644 --- a/src/wxUI/buffers/user_searches.py +++ b/src/wxUI/buffers/user_searches.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -from tweet_searches import searchPanel +from .tweet_searches import searchPanel from multiplatform_widgets import widgets class searchUsersPanel(searchPanel): diff --git a/src/wxUI/dialogs/__init__.py b/src/wxUI/dialogs/__init__.py index a9c57be2..3605a2d1 100644 --- a/src/wxUI/dialogs/__init__.py +++ b/src/wxUI/dialogs/__init__.py @@ -1 +1,2 @@ -import baseDialog, trends, configuration, lists, message, search, find, show_user, update_profile, urlList, userSelection, utils, filterDialogs +from __future__ import absolute_import +from . import baseDialog, trends, configuration, lists, message, search, find, show_user, update_profile, urlList, userSelection, utils, filterDialogs diff --git a/src/wxUI/dialogs/configuration.py b/src/wxUI/dialogs/configuration.py index 3e7e5ff9..dcafb0ec 100644 --- a/src/wxUI/dialogs/configuration.py +++ b/src/wxUI/dialogs/configuration.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import logging as original_logger import wx import application import output import config import widgetUtils -import baseDialog +from . import baseDialog from multiplatform_widgets import widgets class general(wx.Panel, baseDialog.BaseWXDialog): diff --git a/src/wxUI/dialogs/filterDialogs.py b/src/wxUI/dialogs/filterDialogs.py index 6430df19..608ec2c8 100644 --- a/src/wxUI/dialogs/filterDialogs.py +++ b/src/wxUI/dialogs/filterDialogs.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -import baseDialog +from __future__ import absolute_import +# -*- coding: utf-8 -*- +from . import baseDialog import wx import widgetUtils from multiplatform_widgets import widgets diff --git a/src/wxUI/dialogs/find.py b/src/wxUI/dialogs/find.py index 48396295..0b18ea20 100644 --- a/src/wxUI/dialogs/find.py +++ b/src/wxUI/dialogs/find.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -import baseDialog +from __future__ import absolute_import +# -*- coding: utf-8 -*- +from . import baseDialog import wx class findDialog(baseDialog.BaseWXDialog): diff --git a/src/wxUI/dialogs/search.py b/src/wxUI/dialogs/search.py index 12849117..34992613 100644 --- a/src/wxUI/dialogs/search.py +++ b/src/wxUI/dialogs/search.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import widgetUtils -import baseDialog +from . import baseDialog import wx from extra import translator diff --git a/src/wxUI/dialogs/show_user.py b/src/wxUI/dialogs/show_user.py index 74b02070..2fb8d4ca 100644 --- a/src/wxUI/dialogs/show_user.py +++ b/src/wxUI/dialogs/show_user.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -import baseDialog +from . import baseDialog class showUserProfile(baseDialog.BaseWXDialog): def __init__(self): diff --git a/src/wxUI/dialogs/trends.py b/src/wxUI/dialogs/trends.py index 6cf973f6..fa591b36 100644 --- a/src/wxUI/dialogs/trends.py +++ b/src/wxUI/dialogs/trends.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -import baseDialog +from __future__ import absolute_import +# -*- coding: utf-8 -*- +from . import baseDialog import wx class trendingTopicsDialog(baseDialog.BaseWXDialog): diff --git a/src/wxUI/dialogs/update_profile.py b/src/wxUI/dialogs/update_profile.py index d9286516..ea248bc3 100644 --- a/src/wxUI/dialogs/update_profile.py +++ b/src/wxUI/dialogs/update_profile.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import import wx -import baseDialog +from . import baseDialog class updateProfileDialog(baseDialog.BaseWXDialog): def __init__(self): diff --git a/src/wxUI/dialogs/utils.py b/src/wxUI/dialogs/utils.py index fbc1be23..0adb20ca 100644 --- a/src/wxUI/dialogs/utils.py +++ b/src/wxUI/dialogs/utils.py @@ -16,8 +16,9 @@ # along with this program. If not, see . # ############################################################ +from __future__ import absolute_import import wx -import baseDialog +from . import baseDialog class selectUserDialog(baseDialog.BaseWXDialog): def __init__(self, title, users): From 16b2e1661451a9706f2600913ccc9d66cc2501c8 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 22 Nov 2018 17:47:02 -0600 Subject: [PATCH 72/75] Removed libloader from the source repository. #273 --- src/libloader/__init__.py | 7 ----- src/libloader/com.py | 35 ------------------------ src/libloader/libloader.py | 56 -------------------------------------- 3 files changed, 98 deletions(-) delete mode 100644 src/libloader/__init__.py delete mode 100644 src/libloader/com.py delete mode 100644 src/libloader/libloader.py diff --git a/src/libloader/__init__.py b/src/libloader/__init__.py deleted file mode 100644 index b7635386..00000000 --- a/src/libloader/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .libloader import * - -__version__ = 0.1 -__author__ = 'Christopher Toth ' -__doc__ = """ -Quickly and easily load shared libraries from various platforms. Also includes a libloader.com module for loading com modules on Windows. -""" diff --git a/src/libloader/com.py b/src/libloader/com.py deleted file mode 100644 index 1c9a7a6b..00000000 --- a/src/libloader/com.py +++ /dev/null @@ -1,35 +0,0 @@ -from pywintypes import com_error -import win32com -import paths -win32com.__gen_path__=paths.com_path() -import sys -import os -sys.path.append(os.path.join(win32com.__gen_path__, ".")) -from win32com.client import gencache -fixed=False - -def prepare_gencache(): - gencache.is_readonly = False - gencache.GetGeneratePath() - -def patched_getmodule(modname): - mod=__import__(modname) - return sys.modules[modname] - -def load_com(*names): - global fixed - if fixed==False: - gencache._GetModule=patched_getmodule - prepare_gencache() - fixed=True - result = None - for name in names: - try: - result = gencache.EnsureDispatch(name) - break - except com_error: - continue - if result is None: - raise com_error("Unable to load any of the provided com objects.") - return result - diff --git a/src/libloader/libloader.py b/src/libloader/libloader.py deleted file mode 100644 index 174da584..00000000 --- a/src/libloader/libloader.py +++ /dev/null @@ -1,56 +0,0 @@ -import ctypes -import collections -import platform -import os - -TYPES = { - 'Linux': { - 'loader': ctypes.CDLL, - 'functype': ctypes.CFUNCTYPE, - 'prefix': 'lib', - 'extension': '.so' - }, - 'Darwin': { - 'loader': ctypes.CDLL, - 'functype': ctypes.CFUNCTYPE, - 'prefix': 'lib', - 'extension': '.dylib' - }, -} -if platform.system() == 'Windows': - TYPES['Windows'] = { - 'loader': ctypes.WinDLL, - 'functype': ctypes.WINFUNCTYPE, - 'prefix': "", - 'extension': '.dll' - } - -class LibraryLoadError(OSError): pass - -def load_library(library, x86_path='.', x64_path='.', *args, **kwargs): - lib = find_library_path(library, x86_path=x86_path, x64_path=x64_path) - loaded = _do_load(str(lib), *args, **kwargs) - if loaded is not None: - return loaded - raise LibraryLoadError('unable to load %r. Provided library path: %r' % (library, lib)) - -def _do_load(file, *args, **kwargs): - loader = TYPES[platform.system()]['loader'] - return loader(file, *args, **kwargs) - -def find_library_path(libname, x86_path='.', x64_path='.'): - libname = '%s%s' % (TYPES[platform.system()]['prefix'], libname) - if platform.architecture()[0] == '64bit': - path = os.path.join(x64_path, libname) - else: - path = os.path.join(x86_path, libname) - ext = get_library_extension() - path = '%s%s' % (path, ext) - return os.path.abspath(path) - - -def get_functype(): - return TYPES[platform.system()]['functype'] - -def get_library_extension(): - return TYPES[platform.system()]['extension'] From c8d83ed9e7857dc25ab848d884282b5f8a0a5087 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 22 Nov 2018 17:47:38 -0600 Subject: [PATCH 73/75] Added new path to libloader in requirements file #273 --- requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c74b337e..a8dc0ef2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,8 @@ chardet urllib3 youtube-dl python-vlc -pywin32 +pypiwin32 certifi backports.functools_lru_cache -git+https://github.com/manuelcortez/twython \ No newline at end of file +git+https://github.com/manuelcortez/twython +git+https://github.com/manuelcortez/libloader \ No newline at end of file From e5b33160e024330013d7d6abb9e93b487a8570da Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 22 Nov 2018 17:48:22 -0600 Subject: [PATCH 74/75] Added a custom fix for Libloader with changes made by @jmdaweb #273 --- src/fixes/__init__.py | 2 ++ src/fixes/fix_libloader.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/fixes/fix_libloader.py diff --git a/src/fixes/__init__.py b/src/fixes/__init__.py index c14e4d6d..a4ef2a79 100644 --- a/src/fixes/__init__.py +++ b/src/fixes/__init__.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import sys from . import fix_arrow # A few new locales for Three languages in arrow. +from . import fix_libloader # Regenerates comcache properly. from . import fix_urllib3_warnings # Avoiding some SSL warnings related to Twython. from . import fix_win32com from . import fix_requests #fix cacert.pem location for TWBlue binary copies @@ -11,6 +12,7 @@ def setup(): if hasattr(sys, "frozen"): fix_win32com.fix() fix_requests.fix(True) + fix_libloader.fix() else: fix_requests.fix(False) fix_urllib3_warnings.fix() \ No newline at end of file diff --git a/src/fixes/fix_libloader.py b/src/fixes/fix_libloader.py new file mode 100644 index 00000000..67f312a4 --- /dev/null +++ b/src/fixes/fix_libloader.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import win32com +import paths +win32com.__gen_path__=paths.com_path() +import sys +import os +sys.path.append(os.path.join(win32com.__gen_path__, ".")) +from win32com.client import gencache +from pywintypes import com_error +from libloader import com + +fixed=False + +def patched_getmodule(modname): + mod=__import__(modname) + return sys.modules[modname] + +def load_com(*names): + global fixed + if fixed==False: + gencache._GetModule=patched_getmodule + com.prepare_gencache() + fixed=True + result = None + for name in names: + try: + result = gencache.EnsureDispatch(name) + break + except com_error: + continue + if result is None: + raise com_error("Unable to load any of the provided com objects.") + return result + +def fix(): + com.load_com = load_com \ No newline at end of file From d4bf33ca6d7aa8e6a26c1f5b7612f8ec806e6fff Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 25 Nov 2018 13:00:01 -0600 Subject: [PATCH 75/75] Added support for playback from anyaudio.net --- doc/changelog.md | 3 ++- src/audio_services/services.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/changelog.md b/doc/changelog.md index 9de89351..7aecd216 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,7 +2,8 @@ ## changes in this version -* Custom buffer ordering will not be reseted every time the application restarts after an account setting has been modified. +* Added support for playing audios posted in [AnyAudio.net](http://anyaudio.net) directly from TWBlue. Thanks to [Sam Tupy](http://www.samtupy.com/) +* Custom buffer ordering will not be reset every time the application restarts after an account setting has been modified. * When adding or removing an user from a list, it is possible to press enter in the focused list instead of having to search for the "add" or "delete" button. * Quoted and long tweets are displayed properly in the sent tweets buffer after being send. ([#253](https://github.com/manuelcortez/TWBlue/issues/253)) * Fixed an issue that was making the list manager keystroke unable to be shown in the keystroke editor. Now the keystroke is listed properly. ([#260](https://github.com/manuelcortez/TWBlue/issues/260)) diff --git a/src/audio_services/services.py b/src/audio_services/services.py index d57086b7..3860deeb 100644 --- a/src/audio_services/services.py +++ b/src/audio_services/services.py @@ -33,5 +33,12 @@ def convert_soundcloud (url): def convert_youtube_long (url): return youtube_utils.get_video_url(url) +@matches_url ('http://anyaudio.net/listen') +def convert_anyaudio(url): + values = url.split("audio=") + if len(values) != 2: + raise TypeError('%r is not streamable' % url) + return "http://anyaudio.net/audiodownload?audio=%s" % (values[1],) + def convert_generic_audio(url): return url