diff --git a/README.md b/README.md index 34828c7b..591e4485 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ Although most dependencies can be found in the windows-dependencies directory, w #### Dependencies packaged in windows installers -* [Python,](http://python.org) version 2.7.13 +* [Python,](http://python.org) version 2.7.14 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. * [wxPython](http://www.wxpython.org) for Python 2.7, version 3.0.2.0 -* [Python windows extensions (pywin32)](http://www.sourceforge.net/projects/pywin32/) for python 2.7, build 220 +* [Python windows extensions (pywin32)](http://www.sourceforge.net/projects/pywin32/) for python 2.7, build 221 * [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 @@ -85,19 +85,19 @@ This dependency has been built using pure basic 4.61. Its source can be found at #### Dependencies required to build the installer -* [NSIS,](http://nsis.sourceforge.net/) version 3.01 +* [NSIS,](http://nsis.sourceforge.net/) version 3.02.1 #### Dependencies required to build the portableApps.com format archive -* [NSIS Portable,](http://portableapps.com/apps/development/nsis_portable) version 3.0 +* [NSIS Portable,](http://portableapps.com/apps/development/nsis_portable) version 3.02.1 * [PortableApps.com Launcher,](http://portableapps.com/apps/development/portableapps.com_launcher) version 2.2.1 -* [PortableApps.com Installer,](http://portableapps.com/apps/development/portableapps.com_installer) version 3.4.4 +* [PortableApps.com Installer,](http://portableapps.com/apps/development/portableapps.com_installer) version 3.5.5 Important! Install these 3 apps into the same folder, otherwise you won't be able to build the pa.c version. For example: D:\portableApps\NSISPortable, D:\PortableApps\PortableApps.com installer, ... #### Dependencies to make the spell checker multilingual #### -In order to add the support for spell checking in more languages than english you need to add some additional dictionaries to pyenchant. These are located on the dictionaries folder under windows-dependencies. Simply copy them to the share/enchant/myspell folder located in your enchant installation or in the compiled copy of TWBlue. +In order to add the support for spell checking in more languages than english you need to add some additional dictionaries to pyenchant. These are located on the dictionaries folder under windows-dependencies. Simply copy them to the share/enchant/myspell folder located in your enchant installation. They will be automatically copied when building a binary version. ### Running TW Blue from source diff --git a/doc/application.py b/doc/application.py index d88f71a4..2aadae16 100644 --- a/doc/application.py +++ b/doc/application.py @@ -2,7 +2,7 @@ name = 'TWBlue' snapshot = False if snapshot == False: - version = "0.91" + version = "0.92" update_url = 'http://twblue.es/updates/twblue_ngen.json' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/stable.json' else: diff --git a/doc/changelog.md b/doc/changelog.md index ee5921bb..4e5d974c 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,14 @@ ## changes in this version +* When authorising an account, you will see a dialogue with a cancel button, in case you want to abort the process. Also, NVDA will not be blocked when the process starts. ([#101](https://github.com/manuelcortez/TWBlue/issues/101)) +* In the translator module, the list of available languages is fetched automatically from the provider. That means all of these languages will work and there will not be inconsistencies. Also we've removed the first combo box, because the language is detected automatically by Yandex'S API. ([#153](https://github.com/manuelcortez/TWBlue/issues/153)) +* Trending topics, searches and conversation buffers will use mute settings set for the session in wich they were opened. ([#157](https://github.com/manuelcortez/TWBlue/issues/157)) +* And more. ([#156,](https://github.com/manuelcortez/TWBlue/issues/156) [#163,](https://github.com/manuelcortez/TWBlue/issues/163) [#159,](https://github.com/manuelcortez/TWBlue/issues/159)) + +## changes in version 0.91 and 0.92 + +* Fixed incorrect unicode handling when copying tweet to clipboard. ([#150](https://github.com/manuelcortez/TWBlue/issues/150)) * TWBlue will show an error when trying to open a timeline for a suspended user. ([#128](https://github.com/manuelcortez/TWBlue/issues/128)) * Removed TwUp as service as it no longer exists. ([#112](https://github.com/manuelcortez/TWBlue/issues/112)) * Release audio files after uploading them. ([#130](https://github.com/manuelcortez/TWBlue/issues/130)) diff --git a/mysc/pa.c format/App/AppInfo/appinfo.ini b/mysc/pa.c format/App/AppInfo/appinfo.ini index ef1f00fd..351b5faa 100644 --- a/mysc/pa.c format/App/AppInfo/appinfo.ini +++ b/mysc/pa.c format/App/AppInfo/appinfo.ini @@ -20,8 +20,8 @@ CommercialUse=true EULAVersion=2 [Version] -PackageVersion=0.91.0.0 -DisplayVersion=0.91 +PackageVersion=0.92.0.0 +DisplayVersion=0.92 [Control] Icons=1 diff --git a/scripts/twblue.nsi b/scripts/twblue.nsi index dde0a88f..e782546a 100644 --- a/scripts/twblue.nsi +++ b/scripts/twblue.nsi @@ -15,10 +15,10 @@ SetCompressor /solid lzma SetDatablockOptimize on VIAddVersionKey ProductName "TWBlue" VIAddVersionKey LegalCopyright "Copyright 2016 Manuel Cortéz." -VIAddVersionKey ProductVersion "0.91" -VIAddVersionKey FileVersion "0.91" -VIProductVersion "0.91.0.0" -VIFileVersion "0.91.0.0" +VIAddVersionKey ProductVersion "0.92" +VIAddVersionKey FileVersion "0.92" +VIProductVersion "0.92.0.0" +VIFileVersion "0.92.0.0" !insertmacro MUI_PAGE_WELCOME !define MUI_LICENSEPAGE_RADIOBUTTONS !insertmacro MUI_PAGE_LICENSE "license.txt" @@ -72,10 +72,10 @@ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "D WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "UninstallString" '"$INSTDIR\uninstall.exe"' WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "InstallLocation" $INSTDIR WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "Publisher" "Manuel Cortéz" -WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "DisplayVersion" "0.91" +WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "DisplayVersion" "0.92" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "URLInfoAbout" "http://twblue.es" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "VersionMajor" 0 -WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "VersionMinor" 91 +WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "VersionMinor" 92 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\twblue" "NoRepair" 1 SectionEnd diff --git a/src/application.py b/src/application.py index 98f4e5e0..1e384b9c 100644 --- a/src/application.py +++ b/src/application.py @@ -2,7 +2,7 @@ name = 'TWBlue' snapshot = False if snapshot == False: - version = "0.91" + version = "0.92" update_url = 'http://twblue.es/updates/twblue_ngen.json' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/stable.json' else: diff --git a/src/audio_services/services.py b/src/audio_services/services.py index 1db7c9b7..a3bea842 100644 --- a/src/audio_services/services.py +++ b/src/audio_services/services.py @@ -28,21 +28,5 @@ def convert_soundcloud (url): else: raise TypeError('%r is not streamable' % url) -@matches_url('http://twup.me') -def convert_twup(url): - result = re.match("^http://twup.me/(?P[A-Za-z0-9]+/?)$", url, re.I) - if not result or result.group("audio_id") is None: - raise TypeError('%r is not a valid URL' % url) - audio_id = result.group("audio_id") - return 'http://twup.me/%s' % audio_id - -#@matches_url('http://sndup.net') -#def convert_sndup(url): -# result = re.match("^http://sndup.net/(?P[a-z0-9]+/?)(|d|l|a)/?$", url, re.I) -# if not result or result.group("audio_id") is None: -# raise TypeError('%r is not a valid URL' % url) -# audio_id = result.group("audio_id") -# return 'http://sndup.net/%s/a' % audio_id - def convert_generic_audio(url): return url diff --git a/src/controller/buffersController.py b/src/controller/buffersController.py index efce4ee7..8262eda1 100644 --- a/src/controller/buffersController.py +++ b/src/controller/buffersController.py @@ -157,7 +157,7 @@ class bufferController(object): for i in attachments: photo = open(i["file"], "rb") img = self.session.twitter.twitter.upload_media(media=photo) - self.session.twitter.twitter.set_description(media_id=img["media_id"], alt_text=dict(text=i["description"])) + self.session.twitter.twitter.create_metadata(media_id=img["media_id"], alt_text=dict(text=i["description"])) media_ids.append(img["media_id"]) self.session.twitter.twitter.update_status(status=text, media_ids=media_ids) @@ -988,7 +988,7 @@ class searchBufferController(baseBufferController): # return None num = self.session.order_buffer(self.name, val) self.put_items_on_list(num) - if num > 0: + if num > 0 and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"]: self.session.sound.play("search_updated.ogg") return num @@ -1068,7 +1068,7 @@ class searchPeopleBufferController(peopleBufferController): number_of_items = self.session.order_cursored_buffer(self.name, val) log.debug("Number of items retrieved: %d" % (number_of_items,)) self.put_items_on_list(number_of_items) - if number_of_items > 0: + if number_of_items > 0 and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"]: self.session.sound.play("search_updated.ogg") return number_of_items @@ -1148,7 +1148,8 @@ class trendsBufferController(bufferController): self.name_ = data[0]["locations"][0]["name"] self.trends = data[0]["trends"] self.put_items_on_the_list() - self.session.sound.play(self.sound) + if self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"]: + self.session.sound.play(self.sound) def put_items_on_the_list(self): selected_item = self.buffer.list.get_selected() @@ -1270,7 +1271,7 @@ class conversationBufferController(searchBufferController): number_of_items = self.session.order_buffer(self.name, self.statuses) log.debug("Number of items retrieved: %d" % (number_of_items,)) self.put_items_on_list(number_of_items) - if number_of_items > 0: + if number_of_items > 0 and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"]: self.session.sound.play("search_updated.ogg") return number_of_items diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 8b6fa11c..6d565d3a 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -477,12 +477,11 @@ class Controller(object): output.speak(_(u"Empty buffer."), True) return start = page.buffer.list.get_selected() - for i in xrange(start,count): - page.buffer.list.select_item(i) - if string.lower() in page.get_message().lower(): + for i in xrange(start, count): + if string.lower() in page.buffer.list.get_text_column(i, 1).lower(): + page.buffer.list.select_item(i) return output.speak(page.get_message(), True) output.speak(_(u"{0} not found.").format(string,), True) - page.buffer.list.select_item(start) def seekLeft(self, *args, **kwargs): try: @@ -931,8 +930,8 @@ class Controller(object): self.view.insert_buffer(buffer.buffer, name=_(u"Trending topics for %s") % (trends.get_string()), pos=pos) self.buffers.append(buffer) buffer.start_stream() - timer = RepeatingTimer(300, buffer.start_stream) - timer.start() + buffer.timer = RepeatingTimer(300, buffer.start_stream) + buffer.timer.start() buffer.session.settings["other_buffers"]["trending_topic_buffers"].append(woeid) buffer.session.settings.write() @@ -987,9 +986,10 @@ class Controller(object): buff = self.view.search(buffer.name, buffer.account) answer = buffer.remove_buffer() if answer == False: return - if hasattr(buff, "timer"): + log.debug("destroying buffer...") + if hasattr(buffer, "timer"): log.debug("Stopping timer...") - buff.timer.cancel() + buffer.timer.cancel() log.debug("Timer cancelled.") self.right() self.view.delete_buffer(buff) diff --git a/src/controller/messages.py b/src/controller/messages.py index dde3a9ee..52df839b 100644 --- a/src/controller/messages.py +++ b/src/controller/messages.py @@ -40,9 +40,8 @@ class basicTweet(object): dlg = translator.gui.translateDialog() if dlg.get_response() == widgetUtils.OK: text_to_translate = self.message.get_text() - source = [x[0] for x in translator.translator.available_languages()][dlg.get("source_lang")] dest = [x[0] for x in translator.translator.available_languages()][dlg.get("dest_lang")] - msg = translator.translator.translate(text=text_to_translate, source=source, target=dest) + msg = translator.translator.translate(text=text_to_translate, target=dest) self.message.set_text(msg) self.text_processor() self.message.text_focus() diff --git a/src/extra/translator/translator.py b/src/extra/translator/translator.py index 9caa697d..7852741c 100644 --- a/src/extra/translator/translator.py +++ b/src/extra/translator/translator.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- from yandex_translate import YandexTranslate -def translate(text="", source="auto", target="en"): +def translate(text="", target="en"): t = YandexTranslate("trnsl.1.1.20161012T134532Z.d01b9c75fc39aa74.7d1be75a5166a80583eeb020e10f584168da6bf7") - return t.translate(text, target)["text"][0] - + vars = dict(text=text, lang=target) + return t.translate(**vars)["text"][0] +supported_langs = None +d = None languages = { "af": _(u"Afrikaans"), "sq": _(u"Albanian"), @@ -71,7 +73,7 @@ languages = { "ps": _(u"Pashto"), "fa": _(u"Persian"), "pl": _(u"Polish"), - "pt-PT": _(u"Portuguese"), + "pt": _(u"Portuguese"), "pa": _(u"Punjabi"), "ro": _(u"Romanian"), "ru": _(u"Russian"), @@ -101,8 +103,11 @@ languages = { } def available_languages(): - l = languages.keys() - d = languages.values() - l.insert(0, '') - d.insert(0, _(u"autodetect")) - return sorted(zip(l, d)) + global supported_langs, d + if supported_langs == None and d == None: + t = YandexTranslate("trnsl.1.1.20161012T134532Z.d01b9c75fc39aa74.7d1be75a5166a80583eeb020e10f584168da6bf7") + supported_langs = t.langs + d = [] + for i in supported_langs: + d.append(languages[i]) + return sorted(zip(supported_langs, d)) diff --git a/src/extra/translator/wx_ui.py b/src/extra/translator/wx_ui.py index 24689421..a61c9e7e 100644 --- a/src/extra/translator/wx_ui.py +++ b/src/extra/translator/wx_ui.py @@ -25,15 +25,11 @@ class translateDialog(baseDialog.BaseWXDialog): super(translateDialog, self).__init__(None, -1, title=_(u"Translate message")) panel = wx.Panel(self) sizer = wx.BoxSizer(wx.VERTICAL) - staticSource = wx.StaticText(panel, -1, _(u"Source language")) - self.source_lang = wx.ComboBox(panel, -1, choices=[x[1] for x in translator.available_languages()], style = wx.CB_READONLY) - self.source_lang.SetFocus() staticDest = wx.StaticText(panel, -1, _(u"Target language")) - self.source_lang.SetSelection(0) self.dest_lang = wx.ComboBox(panel, -1, choices=[x[1] for x in translator.available_languages()], style = wx.CB_READONLY) + self.dest_lang.SetFocus() + self.dest_lang.SetSelection(0) listSizer = wx.BoxSizer(wx.HORIZONTAL) - listSizer.Add(staticSource) - listSizer.Add(self.source_lang) listSizer.Add(staticDest) listSizer.Add(self.dest_lang) ok = wx.Button(panel, wx.ID_OK) diff --git a/src/sessionmanager/session.py b/src/sessionmanager/session.py index 8422123a..6d57bd02 100644 --- a/src/sessionmanager/session.py +++ b/src/sessionmanager/session.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """ The main session object. Here are the twitter functions to interact with the "model" of TWBlue.""" +import wx import urllib2 import config import twitter @@ -16,10 +17,11 @@ import config_utils import shelve import application import os -from mysc.thread_utils import stream_threaded +from mysc.thread_utils import stream_threaded, call_threaded from pubsub import pub log = logging.getLogger("sessionmanager.session") from long_tweets import tweets, twishort +from wxUI import authorisationDialog sessions = {} @@ -166,7 +168,20 @@ class Session(object): if self.logged == True: raise Exceptions.AlreadyAuthorisedError("The authorisation process is not needed at this time.") else: - self.twitter.authorise(self.settings) + self.authorisation_thread = call_threaded(self.twitter.authorise, self.settings) + self.authorisation_dialog = authorisationDialog() + self.authorisation_dialog.cancel.Bind(wx.EVT_BUTTON, self.authorisation_cancelled) + pub.subscribe(self.authorisation_accepted, "authorisation-accepted") + self.authorisation_dialog.ShowModal() + + def authorisation_cancelled(self, *args, **kwargs): + pub.sendMessage("authorisation-cancelled") + self.authorisation_dialog.Destroy() + del self.authorisation_dialog + + def authorisation_accepted(self): + pub.unsubscribe(self.authorisation_accepted, "authorisation-accepted") + self.authorisation_dialog.Destroy() def get_more_items(self, update_function, users=False, name=None, *args, **kwargs): results = [] diff --git a/src/sessionmanager/wxUI.py b/src/sessionmanager/wxUI.py index c57e1910..61df085e 100644 --- a/src/sessionmanager/wxUI.py +++ b/src/sessionmanager/wxUI.py @@ -73,4 +73,17 @@ class sessionManagerWindow(wx.Dialog): self.configuration.Hide() def destroy(self): - self.Destroy() \ No newline at end of file + self.Destroy() + +class authorisationDialog(wx.Dialog): + def __init__(self): + super(authorisationDialog, self).__init__(parent=None, title=_(u"Authorising account...")) + panel = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + self.text = wx.TextCtrl(panel, -1, _("Waiting for account authorisation..."), style=wx.TE_READONLY|wx.TE_MULTILINE) + self.cancel = wx.Button(panel, wx.ID_CANCEL) + sizer.Add(self.text, 0, wx.ALL, 5) + sizer.Add(self.cancel, 0, wx.ALL, 5) + panel.SetSizer(sizer) + min = sizer.CalcMin() + self.SetClientSize(min) \ No newline at end of file diff --git a/src/sound.py b/src/sound.py index 8a96be44..a80db2a0 100644 --- a/src/sound.py +++ b/src/sound.py @@ -104,7 +104,7 @@ class soundSystem(object): sound_object.play() class URLStream(object): - def __init__(self,url=None): + def __init__(self, url=None): self.url = url self.prepared = False log.debug("URL Player initialized") @@ -118,12 +118,10 @@ class URLStream(object): transformer = audio_services.find_url_transformer(self.url) self.url = transformer(self.url) log.debug("Transformed URL: %s. Prepared" % (self.url,)) - self.prepared = True else: self.url = url log.debug("Transformed URL: %s. Prepared" % (self.url,)) - self.prepared = True - + self.prepared = True def seek(self,step): pos=self.stream.get_position() @@ -154,7 +152,7 @@ class URLStream(object): self.stream.volume = float(volume) self.stream.play() log.debug("played") -# call_threaded(self.delete_when_done) + self.prepared=False def stop_audio(self,delete=False): if hasattr(self, "stream"): @@ -167,6 +165,7 @@ class URLStream(object): if delete: del self.stream log.debug("Deleted audio stream.") + self.prepared=False return True else: return False diff --git a/src/twitter/authorisationHandler.py b/src/twitter/authorisationHandler.py index e7cc668a..b1a6d1f7 100644 --- a/src/twitter/authorisationHandler.py +++ b/src/twitter/authorisationHandler.py @@ -2,11 +2,12 @@ import BaseHTTPServer import application from urlparse import urlparse, parse_qs +from pubsub import pub logged = False verifier = None -class handler(BaseHTTPServer.BaseHTTPRequestHandler): +class handler(BaseHTTPServer.BaseHTTPRequestHandler, object): def do_GET(self): global logged @@ -18,4 +19,14 @@ class handler(BaseHTTPServer.BaseHTTPRequestHandler): global verifier verifier = params.get('oauth_verifier', [None])[0] self.wfile.write(u"You have successfully logged into Twitter with {0}. You can close this window now.".format(application.name)) + pub.sendMessage("authorisation-accepted") + pub.unsubscribe(self.cancelled, "authorisation-cancelled") self.finish() + + def __init__(self, *args, **kwargs): + pub.subscribe(self.cancelled, "authorisation-cancelled") + super(handler, self).__init__(*args, **kwargs) + + def cancelled(self): + pub.unsubscribe(self.cancelled, "authorisation-cancelled") + self.finish() \ No newline at end of file diff --git a/src/twitter/compose.py b/src/twitter/compose.py index 57d521cf..7506c027 100644 --- a/src/twitter/compose.py +++ b/src/twitter/compose.py @@ -184,7 +184,7 @@ def compose_event(data, username, show_screen_names=False): else: event = _(u"%s(@%s) has removed you from the list %s") % (data["source"]["name"], data["source"]["screen_name"], data["target_object"]["name"]) elif data["event"] == "list_user_subscribed": if data["source"]["screen_name"] == username: event = _(u"You've subscribed to the list %s, which is owned by %s(@%s)") % (data["target_object"]["name"], data["target"]["name"], data["target"]["screen_name"]) - else: event = _(u"%s(@%s) has suscribed you to the list %s") % (data["source"]["name"], data["source"]["screen_name"], data["target_object"]["name"]) + else: event = _(u"%s(@%s) has subscribed you to the list %s") % (data["source"]["name"], data["source"]["screen_name"], data["target_object"]["name"]) elif data["event"] == "list_user_unsubscribed": if data["source"]["screen_name"] == username: event = _(u"You've unsubscribed from the list %s, which is owned by %s(@%s)") % (data["target_object"]["name"], data["target"]["name"], data["target"]["screen_name"]) else: event = _("You've been unsubscribed from the list %s, which is owned by %s(@%s)") % (data["target_object"]["name"], data["source"]["name"], data["source"]["screen_name"]) diff --git a/src/twitter/utils.py b/src/twitter/utils.py index 5ed717cf..821fda55 100644 --- a/src/twitter/utils.py +++ b/src/twitter/utils.py @@ -29,7 +29,12 @@ def find_urls (tweet): i = "full_text" else: i = "text" - return [s[0] for s in url_re.findall(tweet[i])] + shorten_urls = find_urls_in_text(tweet[i]) + for url in range(0, len(shorten_urls)): + try: + urls.append(tweet["entities"]["urls"][url]["expanded_url"]) + except: pass + return urls def find_item(id, listItem): for i in range(0, len(listItem)): diff --git a/src/twython/__init__.py b/src/twython/__init__.py index 3525f3c7..84e9ee63 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.3.0' +__version__ = '3.6.0' from .api import Twython from .streaming import TwythonStreamer diff --git a/src/twython/api.py b/src/twython/api.py index fab4a4e8..577d6a6f 100644 --- a/src/twython/api.py +++ b/src/twython/api.py @@ -145,6 +145,7 @@ class Twython(EndpointsMixin, object): 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? @@ -195,17 +196,16 @@ class Twython(EndpointsMixin, object): 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: - # Send the response as is for working with /media/metadata/create.json. - content = response.content -# raise TwythonError('Response was not valid JSON. \ -# Unable to decode.') + if response.content!="": + raise TwythonError('Response was not valid JSON. \ + Unable to decode.') return content @@ -258,8 +258,10 @@ class Twython(EndpointsMixin, object): url = endpoint else: url = '%s/%s.json' % (self.api_url % version, endpoint) + content = self._request(url, method=method, params=params, api_call=url) + return content def get(self, endpoint, params=None, version='1.1'): @@ -473,6 +475,11 @@ class Twython(EndpointsMixin, object): >>> 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__) @@ -531,7 +538,7 @@ class Twython(EndpointsMixin, object): @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 replaced with links) + """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 @@ -547,62 +554,116 @@ class Twython(EndpointsMixin, object): 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: - text = tweet['text'] - entities = tweet['entities'] + # We'll put all the bits of replacement HTML and their starts/ends + # in this list: + entities = [] # Mentions - for entity in sorted(entities['user_mentions'], - key=lambda mention: len(mention['screen_name']), reverse=True): - start, end = entity['indices'][0], entity['indices'][1] + 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' - text = re.sub(r'(?)' + tweet['text'][start:end] + '(?!)', - mention_html % {'screen_name': entity['screen_name']}, text) + mention_html = '@%(screen_name)s' % {'screen_name': entity['screen_name']} + + if display_text_start <= temp['start'] <= display_text_end: + temp['replacement'] = mention_html + 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 - for entity in sorted(entities['hashtags'], - key=lambda hashtag: len(hashtag['text']), reverse=True): - start, end = entity['indices'][0], entity['indices'][1] + if 'hashtags' in tweet['entities']: + for entity in tweet['entities']['hashtags']: + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - hashtag_html = '#%(hashtag)s' - text = re.sub(r'(?)' + tweet['text'][start:end] + '(?!)', - hashtag_html % {'hashtag': entity['text']}, text) + url_html = '#%(hashtag)s' % {'hashtag': entity['text']} - # Urls - for entity in entities['urls']: - start, end = entity['indices'][0], 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'] + temp['replacement'] = url_html + entities.append(temp) - url_html = '%s' - text = text.replace(tweet['text'][start:end], - url_html % (entity['url'], shown_url)) + # Symbols + if 'symbols' in tweet['entities']: + for entity in tweet['entities']['symbols']: + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - # Media - if 'media' in entities: - for entity in entities['media']: - start, end = entity['indices'][0], entity['indices'][1] - if use_display_url and entity.get('display_url') \ - and not use_expanded_url: + 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] + 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' - text = text.replace(tweet['text'][start:end], - url_html % (entity['url'], shown_url)) + url_html = '%s' % (entity['url'], shown_url) - if expand_quoted_status and tweet.get('is_quote_status'): + 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']: + for entity in tweet['entities']['media']: + 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'] - text += '
%(quote)s' \ + quote_text += '
%(quote)s' \ '%(quote_user_name)s' \ '@%(quote_user_screen_name)s' \ '
' % \ @@ -612,4 +673,9 @@ class Twython(EndpointsMixin, object): 'quote_user_name': quoted_status['user']['name'], 'quote_user_screen_name': quoted_status['user']['screen_name']} - return text + 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/endpoints.py b/src/twython/endpoints.py index 7691bfd5..1c1d80c6 100644 --- a/src/twython/endpoints.py +++ b/src/twython/endpoints.py @@ -17,10 +17,12 @@ https://dev.twitter.com/docs/api/1.1 import json import os import warnings -try: - from StringIO import StringIO -except ImportError: - from io import StringIO +from io import BytesIO +from time import sleep +#try: + #from StringIO import StringIO +#except ImportError: + #from io import StringIO from .advisory import TwythonDeprecationWarning @@ -143,15 +145,20 @@ class EndpointsMixin(object): Docs: https://dev.twitter.com/rest/reference/post/media/upload """ + # https://dev.twitter.com/rest/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 set_description(self, **params): - """ Adds a description to an image.""" - # This method only accepts strings, no dictionaries. + 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("media/metadata/create", params=params) + return self.post("https://upload.twitter.com/1.1/media/metadata/create.json", params=params) - def upload_video(self, media, media_type, size=None): + 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. @@ -176,7 +183,8 @@ class EndpointsMixin(object): params = { 'command': 'INIT', 'media_type': media_type, - 'total_bytes': size + 'total_bytes': size, + 'media_category': media_category } response_init = self.post(upload_url, params=params) media_id = response_init['media_id'] @@ -187,7 +195,7 @@ class EndpointsMixin(object): data = media.read(1*1024*1024) if not data: break - media_chunk = StringIO() + media_chunk = BytesIO() media_chunk.write(data) media_chunk.seek(0) @@ -205,7 +213,38 @@ class EndpointsMixin(object): 'command': 'FINALIZE', 'media_id': media_id } - return self.post(upload_url, params=params) + + 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 diff --git a/src/update/updater.py b/src/update/updater.py index c0375d84..2384f968 100644 --- a/src/update/updater.py +++ b/src/update/updater.py @@ -10,11 +10,12 @@ logger = logging.getLogger("updater") def do_update(endpoint=application.update_url): try: - update.perform_update(endpoint=endpoint, current_version=application.version, app_name=application.name, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished) + result = update.perform_update(endpoint=endpoint, current_version=application.version, app_name=application.name, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished) except: if endpoint == application.update_url: logger.error("Update failed! Using mirror URL...") return do_update(endpoint=application.mirror_update_url) else: logger.exception("Update failed.") - output.speak("An exception occurred while attempting to update " + application.name + ". If this message persists, contact the " + application.name + " developers. More information about the exception has been written to the error log.",True) \ No newline at end of file + output.speak("An exception occurred while attempting to update " + application.name + ". If this message persists, contact the " + application.name + " developers. More information about the exception has been written to the error log.",True) + return result \ No newline at end of file diff --git a/src/url_shortener/__main__.py b/src/url_shortener/__main__.py index 56080ce7..216761d4 100644 --- a/src/url_shortener/__main__.py +++ b/src/url_shortener/__main__.py @@ -25,7 +25,7 @@ def unshorten (url, service=None, **kwargs): def default_service (): - return shorteners.TinyurlShortener + return shorteners.AcortameShortener def find_service (service, **kwargs): for i in shorteners.__all__: diff --git a/src/url_shortener/shorteners/__init__.py b/src/url_shortener/shorteners/__init__.py index 53dce662..fc1bfc14 100644 --- a/src/url_shortener/shorteners/__init__.py +++ b/src/url_shortener/shorteners/__init__.py @@ -6,4 +6,5 @@ from tinyarrows import TinyArrowsShortener from tinyurl import TinyurlShortener from xedcc import XedccShortener from clckru import ClckruShortener -__all__ = ["HKCShortener", "IsgdShortener", "OnjmeShortener", "TinyArrowsShortener", "TinyurlShortener", "XedccShortener", "ClckruShortener"] +from acortame import AcortameShortener +__all__ = ["HKCShortener", "IsgdShortener", "OnjmeShortener", "TinyArrowsShortener", "TinyurlShortener", "XedccShortener", "ClckruShortener", "AcortameShortener"] diff --git a/src/url_shortener/shorteners/acortame.py b/src/url_shortener/shorteners/acortame.py new file mode 100644 index 00000000..4cd274d2 --- /dev/null +++ b/src/url_shortener/shorteners/acortame.py @@ -0,0 +1,27 @@ +from url_shortener import URLShortener +import requests +import urllib +class AcortameShortener (URLShortener): + def __init__(self, *args, **kwargs): + self.name = "acorta.me" + super(AcortameShortener, self).__init__(*args, **kwargs) + + def _shorten (self, url): + answer = url + api = requests.get ("https://acorta.me/api.php?action=shorturl&format=simple&url=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text + return answer + + def created_url (self, url): + return 'acorta.me' in url + + def unshorten (self, url): + if not 'acorta.me' in url: + #use generic expand method + return super(AcortameShortener, self).unshorten(url) + answer = url + api = requests.get ("https://acorta.me/api.php?action=expand&format=simple&shorturl=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text + return answer diff --git a/src/url_shortener/shorteners/clckru.py b/src/url_shortener/shorteners/clckru.py index f72f9281..ba1d8bb2 100644 --- a/src/url_shortener/shorteners/clckru.py +++ b/src/url_shortener/shorteners/clckru.py @@ -1,19 +1,18 @@ import urllib - +import requests from url_shortener import URLShortener class ClckruShortener (URLShortener): def __init__ (self, *args, **kwargs): self.name = "clck.ru" - return super(ClckruShortener, self).__init__(*args, **kwargs) + super(ClckruShortener, self).__init__(*args, **kwargs) def _shorten (self, url): answer = url - api = urllib.urlopen ("http://clck.ru/--?url=" + urllib.quote(url)) - if api.getcode() == 200: - answer = api.read() - api.close() + api = requests.get ("http://clck.ru/--?url=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text return answer def created_url (self, url): diff --git a/src/url_shortener/shorteners/hkcim.py b/src/url_shortener/shorteners/hkcim.py index 3a47e16f..4c27ec82 100644 --- a/src/url_shortener/shorteners/hkcim.py +++ b/src/url_shortener/shorteners/hkcim.py @@ -1,5 +1,5 @@ import urllib - +import requests from url_shortener import URLShortener class HKCShortener (URLShortener): @@ -9,10 +9,9 @@ class HKCShortener (URLShortener): def _shorten (self, url): answer = url - api = urllib.urlopen ("http://hkc.im/yourls-api.php?action=shorturl&format=simple&url=" + urllib.quote(url)) - if api.getcode() == 200: - answer = api.read() - api.close() + api = requests.get ("http://hkc.im/yourls-api.php?action=shorturl&format=simple&url=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text return answer def created_url (self, url): diff --git a/src/url_shortener/shorteners/isgd.py b/src/url_shortener/shorteners/isgd.py index d2d25b33..ce46890f 100644 --- a/src/url_shortener/shorteners/isgd.py +++ b/src/url_shortener/shorteners/isgd.py @@ -1,19 +1,18 @@ import urllib - +import requests from url_shortener import URLShortener class IsgdShortener (URLShortener): def __init__ (self, *args, **kwargs): self.name = "Is.gd" - return super(IsgdShortener, self).__init__(*args, **kwargs) + super(IsgdShortener, self).__init__(*args, **kwargs) def _shorten (self, url): answer = url - api = urllib.urlopen ("http://is.gd/api.php?longurl=" + urllib.quote(url)) - if api.getcode() == 200: - answer = api.read() - api.close() + api = requests.get ("http://is.gd/api.php?longurl=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text return answer def created_url (self, url): diff --git a/src/url_shortener/shorteners/onjme.py b/src/url_shortener/shorteners/onjme.py index 0b0fd5a2..1a2ec992 100644 --- a/src/url_shortener/shorteners/onjme.py +++ b/src/url_shortener/shorteners/onjme.py @@ -1,5 +1,5 @@ import urllib - +import requests from url_shortener import URLShortener class OnjmeShortener (URLShortener): @@ -9,10 +9,9 @@ class OnjmeShortener (URLShortener): def _shorten (self, url): answer = url - api = urllib.urlopen ("http://onj.me/yourls-api.php?action=shorturl&format=simple&url=" + urllib.quote(url)) - if api.getcode() == 200: - answer = api.read() - api.close() + api = requests.get ("http://onj.me/yourls-api.php?action=shorturl&format=simple&url=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text return answer def created_url (self, url): diff --git a/src/url_shortener/shorteners/tinyarrows.py b/src/url_shortener/shorteners/tinyarrows.py index d9392fb7..cce2847c 100644 --- a/src/url_shortener/shorteners/tinyarrows.py +++ b/src/url_shortener/shorteners/tinyarrows.py @@ -1,5 +1,5 @@ import urllib - +import requests from url_shortener import URLShortener class TinyArrowsShortener (URLShortener): @@ -9,8 +9,10 @@ class TinyArrowsShortener (URLShortener): def _shorten (self, url): answer = url - answer = urllib.urlopen("http://tinyarro.ws/api-create.php?utfpure=1&url=%s" % urllib.quote(url)).read() + api = requests.get("http://tinyarro.ws/api-create.php?utfpure=1&url=%s" % urllib.quote(url)) + if api.status_code == 200: + answer = api.text return answer.decode('UTF-8') def created_url(self, url): - return False + return "tinyarro.ws" in url diff --git a/src/url_shortener/shorteners/tinyurl.py b/src/url_shortener/shorteners/tinyurl.py index 6f1b316e..79a7012e 100644 --- a/src/url_shortener/shorteners/tinyurl.py +++ b/src/url_shortener/shorteners/tinyurl.py @@ -1,4 +1,5 @@ from url_shortener import URLShortener +import requests import urllib class TinyurlShortener (URLShortener): def __init__(self, *args, **kwargs): @@ -6,12 +7,10 @@ class TinyurlShortener (URLShortener): super(TinyurlShortener, self).__init__(*args, **kwargs) def _shorten (self, url): - answer = url - api = urllib.urlopen ("http://tinyurl.com/api-create.php?url=" + urllib.quote(url)) - if api.getcode() == 200: - answer = api.read() - api.close() + api = requests.get ("http://tinyurl.com/api-create.php?url=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text return answer def created_url (self, url): diff --git a/src/url_shortener/shorteners/url_shortener.py b/src/url_shortener/shorteners/url_shortener.py index e0b25561..6388dc0d 100644 --- a/src/url_shortener/shorteners/url_shortener.py +++ b/src/url_shortener/shorteners/url_shortener.py @@ -1,6 +1,4 @@ -from httplib import HTTPConnection -from urlparse import urlparse - +import requests class URLShortener (object): @@ -22,12 +20,18 @@ class URLShortener (object): raise NotImplementedError def unshorten(self, url): - working = urlparse(url) - if not working.netloc: - raise TypeError, "Unable to parse URL." - con = HTTPConnection(working.netloc) - con.connect() - con.request('GET', working.path) - resp = con.getresponse() - con.close() - return resp.getheader('location') + try: + r=requests.head(url) + if 'location' in r.headers.keys(): + if 'dropbox.com' in r.headers['location']: + return handle_dropbox(r.headers['location']) + else: + return r.headers['location'] + except: + return url #we cannot expand + +def handle_dropbox(url): + if url.endswith("dl=1"): + return url + else: + return url.replace("dl=0", "dl=1") diff --git a/src/url_shortener/shorteners/xedcc.py b/src/url_shortener/shorteners/xedcc.py index 8d466306..6741604e 100644 --- a/src/url_shortener/shorteners/xedcc.py +++ b/src/url_shortener/shorteners/xedcc.py @@ -1,5 +1,5 @@ import urllib - +import requests from url_shortener import URLShortener class XedccShortener (URLShortener): @@ -9,10 +9,9 @@ class XedccShortener (URLShortener): def _shorten (self, url): answer = url - api = urllib.urlopen ("http://xed.cc/yourls-api.php?action=shorturl&format=simple&url=" + urllib.quote(url)) - if api.getcode() == 200: - answer = api.read() - api.close() + api = requests.get ("http://xed.cc/yourls-api.php?action=shorturl&format=simple&url=" + urllib.quote(url)) + if api.status_code == 200: + answer = api.text return answer def created_url (self, url): diff --git a/src/wxUI/dialogs/search.py b/src/wxUI/dialogs/search.py index e04f07c6..12849117 100644 --- a/src/wxUI/dialogs/search.py +++ b/src/wxUI/dialogs/search.py @@ -26,8 +26,7 @@ class searchDialog(baseDialog.BaseWXDialog): radioSizer.Add(self.users, 0, wx.ALL, 5) sizer.Add(radioSizer, 0, wx.ALL, 5) lang = wx.StaticText(panel, -1, _(u"&Language for results: ")) - langs = [x[1] for x in translator.translator.available_languages()] - langs[:] = langs[1:] + langs = [x for x in translator.translator.languages.values()] langs.insert(0, _(u"any")) self.lang = wx.ComboBox(panel, -1, choices=langs, value=langs[0], style = wx.CB_READONLY) langBox = wx.BoxSizer(wx.HORIZONTAL) @@ -51,7 +50,12 @@ class searchDialog(baseDialog.BaseWXDialog): self.SetClientSize(sizer.CalcMin()) def get_language(self): - return [x[0] for x in translator.translator.available_languages()][self.lang.GetSelection()] + l = self.lang.GetStringSelection() + if l == _(u"any"): + return "" + for langcode, langname in translator.translator.languages.iteritems(): + if langname == l: + return langcode def get_result_type(self): r = self.resultstype.GetValue() diff --git a/updates/stable.json b/updates/stable.json index f795013a..cb9f8c0c 100644 --- a/updates/stable.json +++ b/updates/stable.json @@ -1,4 +1,4 @@ -{"current_version": "0.91", +{"current_version": "0.92", "description": "The first version for the new generation of TWBlue.", "date": "day_name_abr month day_numb, 2016", "downloads": diff --git a/windows-dependencies b/windows-dependencies index f8bd2d8c..990bb47a 160000 --- a/windows-dependencies +++ b/windows-dependencies @@ -1 +1 @@ -Subproject commit f8bd2d8c6ce174f31cb28c47172935a5162af546 +Subproject commit 990bb47acc45df5bc7996aae676c27a4a3467edd