├── .gitattributes ├── .gitignore ├── GUI ├── account_options.py ├── accounts.py ├── ask.py ├── chooser.py ├── invisible.py ├── lists.py ├── main.py ├── misc.py ├── options.py ├── poll.py ├── profile.py ├── search.py ├── timelines.py ├── tray.py ├── tweet.py └── view.py ├── README.md ├── SAAPI64.dll ├── Tolk.dll ├── Tolk.py ├── application.py ├── build.bat ├── building and running Quinter on m1.txt ├── compile.command ├── compileM1.command ├── copy.bat ├── docs ├── Readme.url └── changelog.txt ├── globals.py ├── keymac.keymap ├── keymap.keymap ├── nvdaControllerClient64.dll ├── quinter.pyw ├── quinter_updater ├── unzip.exe ├── updater.exe └── updater.pb ├── requirements.txt ├── run.bat ├── sound.py ├── sounds └── default │ ├── boundary.ogg │ ├── close.ogg │ ├── delete.ogg │ ├── error.ogg │ ├── follow.ogg │ ├── home.ogg │ ├── like.ogg │ ├── likes.ogg │ ├── list.ogg │ ├── max_length.ogg │ ├── media.ogg │ ├── mentions.ogg │ ├── messages.ogg │ ├── new.ogg │ ├── open.ogg │ ├── ready.ogg │ ├── search.ogg │ ├── send_message.ogg │ ├── send_reply.ogg │ ├── send_retweet.ogg │ ├── send_tweet.ogg │ ├── unfollow.ogg │ ├── unlike.ogg │ ├── user.ogg │ └── volume_changed.ogg ├── speak.py ├── streaming.py ├── timeline.py ├── twishort.py ├── twitter.py └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | windist 2 | macdist 3 | __pycache__ 4 | *.log 5 | *.spec 6 | *.pyc 7 | QPlay.exe 8 | quinter_updater\updater.exe -------------------------------------------------------------------------------- /GUI/account_options.py: -------------------------------------------------------------------------------- 1 | from sound_lib import stream 2 | import platform 3 | import os, sys 4 | import globals 5 | import wx 6 | from . import main 7 | 8 | class general(wx.Panel, wx.Dialog): 9 | def __init__(self, account, parent): 10 | self.snd = stream.FileStream(file=globals.confpath+"/sounds/default/boundary.ogg") 11 | self.account=account 12 | super(general, self).__init__(parent) 13 | self.main_box = wx.BoxSizer(wx.VERTICAL) 14 | self.soundpack_box = wx.BoxSizer(wx.VERTICAL) 15 | self.soundpacklist_label=wx.StaticText(self, -1, "Soundpacks") 16 | self.soundpackslist = wx.ListBox(self, -1) 17 | self.soundpack_box.Add(self.soundpackslist, 0, wx.ALL, 10) 18 | self.soundpackslist.Bind(wx.EVT_LISTBOX, self.on_soundpacks_list_change) 19 | dirs = os.listdir(globals.confpath+"/sounds") 20 | for i in range(0,len(dirs)): 21 | if not dirs[i].startswith("_") and not dirs[i].startswith(".DS"): 22 | self.soundpackslist.Insert(dirs[i],self.soundpackslist.GetCount()) 23 | if account.prefs.soundpack==dirs[i]: 24 | self.soundpackslist.SetSelection(self.soundpackslist.GetCount()-1) 25 | self.sp=dirs[i] 26 | try: 27 | dirs2 = os.listdir("sounds") 28 | for i in range(0,len(dirs2)): 29 | if not dirs2[i].startswith("_") and not dirs2[i].startswith(".DS") and dirs2[i] not in dirs: 30 | self.soundpackslist.Insert(dirs2[i],self.soundpackslist.GetCount()) 31 | if account.prefs.soundpack==dirs2[i]: 32 | self.soundpackslist.SetSelection(self.soundpackslist.GetCount()-1) 33 | self.sp=dirs2[i] 34 | except: 35 | pass 36 | if not hasattr(self,"sp"): 37 | self.sp="default" 38 | self.text_label = wx.StaticText(self, -1, "Sound pan") 39 | self.soundpan = wx.Slider(self, -1, self.account.prefs.soundpan*50,-50,50,name="Soundpack Pan") 40 | self.soundpan.Bind(wx.EVT_SLIDER,self.OnPan) 41 | self.main_box.Add(self.soundpan, 0, wx.ALL, 10) 42 | self.text_label = wx.StaticText(self, -1, "Tweet Footer (Optional)") 43 | self.footer = wx.TextCtrl(self, -1, "",style=wx.TE_MULTILINE) 44 | self.main_box.Add(self.footer, 0, wx.ALL, 10) 45 | self.footer.AppendText(account.prefs.footer) 46 | self.footer.SetMaxLength(280) 47 | 48 | def OnPan(self,event): 49 | pan=self.soundpan.GetValue()/50 50 | self.snd.pan=pan 51 | self.snd.play() 52 | 53 | def on_soundpacks_list_change(self, event): 54 | self.sp=event.GetString() 55 | 56 | class OptionsGui(wx.Dialog): 57 | def __init__(self,account): 58 | self.account=account 59 | wx.Dialog.__init__(self, None, title="Account Options for "+self.account.me.screen_name, size=(350,200)) # initialize the wx frame 60 | self.Bind(wx.EVT_CLOSE, self.OnClose) 61 | self.panel = wx.Panel(self) 62 | self.main_box = wx.BoxSizer(wx.VERTICAL) 63 | self.notebook = wx.Notebook(self.panel) 64 | self.general=general(self.account, self.notebook) 65 | self.notebook.AddPage(self.general, "General") 66 | self.general.SetFocus() 67 | self.main_box.Add(self.notebook, 0, wx.ALL, 10) 68 | self.ok = wx.Button(self.panel, wx.ID_OK, "&OK") 69 | self.ok.SetDefault() 70 | self.ok.Bind(wx.EVT_BUTTON, self.OnOK) 71 | self.main_box.Add(self.ok, 0, wx.ALL, 10) 72 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 73 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 74 | self.main_box.Add(self.close, 0, wx.ALL, 10) 75 | self.panel.Layout() 76 | 77 | def OnOK(self, event): 78 | self.account.prefs.soundpack=self.general.sp 79 | self.account.prefs.soundpan=self.general.soundpan.GetValue()/50 80 | self.account.prefs.footer=self.general.footer.GetValue() 81 | self.general.snd.free() 82 | self.Destroy() 83 | 84 | def OnClose(self, event): 85 | self.general.snd.free() 86 | self.Destroy() 87 | -------------------------------------------------------------------------------- /GUI/accounts.py: -------------------------------------------------------------------------------- 1 | import application 2 | import wx 3 | import globals 4 | from . import main, misc 5 | 6 | class AccountsGui(wx.Dialog): 7 | def __init__(self): 8 | wx.Dialog.__init__(self, None, title="Accounts", size=(350,200)) 9 | self.Bind(wx.EVT_CLOSE, self.OnClose) 10 | self.panel = wx.Panel(self) 11 | self.main_box = wx.BoxSizer(wx.VERTICAL) 12 | self.list_label=wx.StaticText(self.panel, -1, label="&Accounts") 13 | self.list=wx.ListBox(self.panel, -1) 14 | self.main_box.Add(self.list, 0, wx.ALL, 10) 15 | self.list.SetFocus() 16 | self.list.Bind(wx.EVT_LISTBOX, self.on_list_change) 17 | self.add_items() 18 | self.load = wx.Button(self.panel, wx.ID_DEFAULT, "&Switch") 19 | self.load.SetDefault() 20 | self.load.Bind(wx.EVT_BUTTON, self.Load) 21 | # self.load.Enable(False) 22 | self.main_box.Add(self.load, 0, wx.ALL, 10) 23 | self.new = wx.Button(self.panel, wx.ID_DEFAULT, "&Add account") 24 | self.new.Bind(wx.EVT_BUTTON, self.New) 25 | self.main_box.Add(self.new, 0, wx.ALL, 10) 26 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 27 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 28 | self.main_box.Add(self.close, 0, wx.ALL, 10) 29 | self.panel.Layout() 30 | 31 | def add_items(self): 32 | index=0 33 | for i in globals.accounts: 34 | self.list.Insert(i.me.screen_name,self.list.GetCount()) 35 | if i==globals.currentAccount: 36 | self.list.SetSelection(index) 37 | index+=1 38 | 39 | def on_list_change(self,event): 40 | pass 41 | 42 | def New(self, event): 43 | globals.add_session() 44 | globals.prefs.accounts+=1 45 | globals.currentAccount=globals.accounts[len(globals.accounts)-1] 46 | main.window.refreshTimelines() 47 | main.window.on_list_change(None) 48 | main.window.SetLabel(globals.currentAccount.me.screen_name+" - "+application.name+" "+application.version) 49 | self.Destroy() 50 | 51 | def Load(self, event): 52 | globals.currentAccount=globals.accounts[self.list.GetSelection()] 53 | main.window.refreshTimelines() 54 | main.window.list.SetSelection(globals.currentAccount.currentIndex) 55 | main.window.on_list_change(None) 56 | main.window.SetLabel(globals.currentAccount.me.screen_name+" - "+application.name+" "+application.version) 57 | self.Destroy() 58 | 59 | def OnClose(self, event): 60 | self.Destroy() 61 | -------------------------------------------------------------------------------- /GUI/ask.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | def ask(parent=None, message="", caption="", default_value=""): 4 | dlg = wx.TextEntryDialog(parent, caption, message, value=default_value) 5 | dlg.ShowModal() 6 | result = dlg.GetValue() 7 | dlg.Destroy() 8 | return result 9 | 10 | app = wx.App() 11 | -------------------------------------------------------------------------------- /GUI/chooser.py: -------------------------------------------------------------------------------- 1 | from tweepy import TweepyException 2 | import globals, sound, timeline, utils 3 | import time 4 | import wx 5 | import webbrowser 6 | import os 7 | import platform 8 | from . import lists, main, misc, view 9 | 10 | class ChooseGui(wx.Dialog): 11 | 12 | #constants for the types we might need to handle 13 | TYPE_BLOCK="block" 14 | TYPE_FOLLOW="follow" 15 | TYPE_LIST = "list" 16 | TYPE_LIST_R="listr" 17 | TYPE_MUTE="mute" 18 | TYPE_PROFILE = "profile" 19 | TYPE_UNBLOCK="unblock" 20 | TYPE_UNFOLLOW="unfollow" 21 | TYPE_UNMUTE="unmute" 22 | TYPE_URL="url" 23 | TYPE_USER_TIMELINE="userTimeline" 24 | 25 | def __init__(self,account,title="Choose",text="Choose a thing",list=[],type=""): 26 | self.account=account 27 | self.type=type 28 | self.returnvalue="" 29 | wx.Dialog.__init__(self, None, title=title, size=(350,200)) 30 | self.Bind(wx.EVT_CLOSE, self.OnClose) 31 | self.panel = wx.Panel(self) 32 | self.main_box = wx.BoxSizer(wx.VERTICAL) 33 | self.chooser_label=wx.StaticText(self.panel, -1, title) 34 | self.chooser=wx.ComboBox(self.panel,-1,size=(800,600)) 35 | self.main_box.Add(self.chooser, 0, wx.ALL, 10) 36 | self.chooser.SetFocus() 37 | for i in list: 38 | self.chooser.Insert(i,self.chooser.GetCount()) 39 | self.chooser.SetSelection(0) 40 | self.ok = wx.Button(self.panel, wx.ID_DEFAULT, "OK") 41 | self.ok.SetDefault() 42 | self.ok.Bind(wx.EVT_BUTTON, self.OK) 43 | self.main_box.Add(self.ok, 0, wx.ALL, 10) 44 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 45 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 46 | self.main_box.Add(self.close, 0, wx.ALL, 10) 47 | self.panel.Layout() 48 | 49 | def OK(self, event): 50 | self.returnvalue=self.chooser.GetValue().strip("@") 51 | self.Destroy() 52 | if self.type==self.TYPE_PROFILE: 53 | user=view.UserViewGui(self.account,[utils.lookup_user_name(self.account,self.returnvalue)],self.returnvalue+"'s profile") 54 | user.Show() 55 | elif self.type==self.TYPE_URL: 56 | utils.openURL(self.returnvalue) 57 | elif self.type==self.TYPE_LIST: 58 | l=lists.ListsGui(self.account,utils.lookup_user_name(self.account,self.returnvalue)) 59 | l.Show() 60 | elif self.type==self.TYPE_LIST_R: 61 | l=lists.ListsGui(self.account,utils.lookup_user_name(self.account,self.returnvalue),False) 62 | l.Show() 63 | elif self.type==self.TYPE_FOLLOW: 64 | misc.follow_user(self.account,self.returnvalue) 65 | elif self.type==self.TYPE_UNFOLLOW: 66 | misc.unfollow_user(self.account,self.returnvalue) 67 | elif self.type==self.TYPE_BLOCK: 68 | user=self.account.block(self.returnvalue) 69 | elif self.type==self.TYPE_UNBLOCK: 70 | user=self.account.unblock(self.returnvalue) 71 | elif self.type==self.TYPE_MUTE: 72 | try: 73 | user=self.account.api.create_mute(screen_name=self.returnvalue) 74 | except TweepyException as e: 75 | utils.handle_error(e,"Mute") 76 | elif self.type==self.TYPE_UNMUTE: 77 | try: 78 | user=self.account.api.destroy_mute(screen_name=self.returnvalue) 79 | except TweepyException as e: 80 | utils.handle_error(e,"Unmute") 81 | elif self.type==self.TYPE_USER_TIMELINE: 82 | misc.user_timeline_user(self.account,self.returnvalue) 83 | 84 | def OnClose(self, event): 85 | self.Destroy() 86 | 87 | def chooser(account,title="choose",text="Choose some stuff",list=[],type=""): 88 | chooser=ChooseGui(account,title,text,list,type) 89 | chooser.Show() 90 | return chooser.returnvalue -------------------------------------------------------------------------------- /GUI/invisible.py: -------------------------------------------------------------------------------- 1 | import globals 2 | from . import main 3 | import speak 4 | import utils 5 | import sound 6 | def register_key(key,name,reg=True): 7 | if hasattr(main.window,name): 8 | try: 9 | if reg: 10 | main.window.handler.register_key(key,getattr(main.window,name)) 11 | else: 12 | main.window.handler.unregister_key(key,getattr(main.window,name)) 13 | return True 14 | except: 15 | return False 16 | if hasattr(main.window,"on"+name): 17 | try: 18 | if reg: 19 | main.window.handler.register_key(key,getattr(main.window,"on"+name)) 20 | else: 21 | main.window.handler.unregister_key(key,getattr(main.window,"on"+name)) 22 | return True 23 | except: 24 | return False 25 | if hasattr(main.window,"On"+name): 26 | try: 27 | if reg: 28 | main.window.handler.register_key(key,getattr(main.window,"On"+name)) 29 | else: 30 | main.window.handler.unregister_key(key,getattr(main.window,"On"+name)) 31 | return True 32 | except: 33 | return False 34 | if hasattr(inv,name): 35 | try: 36 | if reg: 37 | main.window.handler.register_key(key,getattr(inv,name)) 38 | else: 39 | main.window.handler.unregister_key(key,getattr(inv,name)) 40 | return True 41 | except: 42 | return False 43 | 44 | class invisible_interface(object): 45 | def focus_tl(self,sync=False): 46 | globals.currentAccount.currentTimeline=globals.currentAccount.list_timelines()[globals.currentAccount.currentIndex] 47 | if not sync and globals.prefs.invisible_sync or sync: 48 | main.window.list.SetSelection(globals.currentAccount.currentIndex) 49 | main.window.on_list_change(None) 50 | extratext="" 51 | if globals.prefs.position: 52 | if len(globals.currentAccount.currentTimeline.statuses)==0: 53 | extratext+="Empty" 54 | else: 55 | extratext+=str(globals.currentAccount.currentTimeline.index+1)+" of "+str(len(globals.currentAccount.currentTimeline.statuses)) 56 | if globals.currentAccount.currentTimeline.read: 57 | extratext+=", Autoread" 58 | if globals.currentAccount.currentTimeline.mute: 59 | extratext+=", muted" 60 | speak.speak(globals.currentAccount.currentTimeline.name+". "+extratext,True) 61 | if not globals.prefs.invisible_sync and not sync: 62 | main.window.play_earcon() 63 | 64 | def focus_tl_item(self): 65 | if globals.prefs.invisible_sync: 66 | main.window.list2.SetSelection(globals.currentAccount.currentTimeline.index) 67 | main.window.on_list2_change(None) 68 | else: 69 | if globals.prefs.earcon_audio and len(sound.get_media_urls(utils.find_urls_in_tweet(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]))) > 0: 70 | sound.play(globals.currentAccount,"media") 71 | self.speak_item() 72 | 73 | def speak_item(self): 74 | if globals.currentAccount.currentTimeline.type!="messages": 75 | speak.speak(utils.process_tweet(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]),True) 76 | else: 77 | speak.speak(utils.process_message(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]),True) 78 | 79 | def prev_tl(self,sync=False): 80 | globals.currentAccount.currentIndex-=1 81 | if globals.currentAccount.currentIndex<0: 82 | globals.currentAccount.currentIndex=len(globals.currentAccount.list_timelines())-1 83 | self.focus_tl(sync) 84 | 85 | def next_tl(self,sync=False): 86 | globals.currentAccount.currentIndex+=1 87 | if globals.currentAccount.currentIndex>=len(globals.currentAccount.list_timelines()): 88 | globals.currentAccount.currentIndex=0 89 | self.focus_tl(sync) 90 | 91 | def prev_item(self): 92 | if globals.currentAccount.currentTimeline.index==0 or len(globals.currentAccount.currentTimeline.statuses)==0: 93 | sound.play(globals.currentAccount,"boundary") 94 | if globals.prefs.repeat: 95 | self.speak_item() 96 | return 97 | globals.currentAccount.currentTimeline.index-=1 98 | self.focus_tl_item() 99 | 100 | def prev_item_jump(self): 101 | if globals.currentAccount.currentTimeline.index < 20: 102 | sound.play(globals.currentAccount,"boundary") 103 | if globals.prefs.repeat: 104 | self.speak_item() 105 | return 106 | globals.currentAccount.currentTimeline.index -= 20 107 | self.focus_tl_item() 108 | 109 | def top_item(self): 110 | globals.currentAccount.currentTimeline.index=0 111 | self.focus_tl_item() 112 | 113 | def next_item(self): 114 | if globals.currentAccount.currentTimeline.index==len(globals.currentAccount.currentTimeline.statuses)-1 or len(globals.currentAccount.currentTimeline.statuses)==0: 115 | sound.play(globals.currentAccount,"boundary") 116 | if globals.prefs.repeat: 117 | self.speak_item() 118 | return 119 | globals.currentAccount.currentTimeline.index+=1 120 | self.focus_tl_item() 121 | 122 | def next_item_jump(self): 123 | if globals.currentAccount.currentTimeline.index >= len(globals.currentAccount.currentTimeline.statuses) - 20: 124 | sound.play(globals.currentAccount,"boundary") 125 | if globals.prefs.repeat: 126 | self.speak_item() 127 | return 128 | globals.currentAccount.currentTimeline.index += 20 129 | self.focus_tl_item() 130 | 131 | def bottom_item(self): 132 | globals.currentAccount.currentTimeline.index=len(globals.currentAccount.currentTimeline.statuses)-1 133 | self.focus_tl_item() 134 | 135 | def previous_from_user(self): 136 | main.window.OnPreviousFromUser() 137 | self.speak_item() 138 | 139 | def next_from_user(self): 140 | main.window.OnNextFromUser() 141 | self.speak_item() 142 | 143 | def previous_in_thread(self): 144 | main.window.OnPreviousInThread() 145 | self.speak_item() 146 | 147 | def next_in_thread(self): 148 | main.window.OnNextInThread() 149 | self.speak_item() 150 | 151 | def refresh(self,event=None): 152 | globals.currentAccount.currentTimeline.load(speech=True) 153 | 154 | def speak_account(self): 155 | speak.speak(globals.currentAccount.me.screen_name) 156 | 157 | inv=invisible_interface() -------------------------------------------------------------------------------- /GUI/lists.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import globals 3 | from . import misc 4 | from . import view 5 | 6 | class ListsGui(wx.Dialog): 7 | def __init__(self,account,user=None,add=True): 8 | self.account=account 9 | self.add=add 10 | self.user=user 11 | self.lists=self.account.api.get_lists() 12 | wx.Dialog.__init__(self, None, title="Lists", size=(350,200)) 13 | self.Bind(wx.EVT_CLOSE, self.OnClose) 14 | self.panel = wx.Panel(self) 15 | self.main_box = wx.BoxSizer(wx.VERTICAL) 16 | self.list_label=wx.StaticText(self.panel, -1, label="&Lists") 17 | self.list=wx.ListBox(self.panel, -1) 18 | self.main_box.Add(self.list, 0, wx.ALL, 10) 19 | self.list.SetFocus() 20 | self.list.Bind(wx.EVT_LISTBOX, self.on_list_change) 21 | self.add_items() 22 | if self.user!=None: 23 | if self.add: 24 | self.load = wx.Button(self.panel, wx.ID_DEFAULT, "&Add") 25 | else: 26 | self.load = wx.Button(self.panel, wx.ID_DEFAULT, "&Remove") 27 | else: 28 | self.load = wx.Button(self.panel, wx.ID_DEFAULT, "&Load list") 29 | self.load.SetDefault() 30 | self.load.Bind(wx.EVT_BUTTON, self.Load) 31 | self.load.Enable(False) 32 | self.main_box.Add(self.load, 0, wx.ALL, 10) 33 | if len(self.lists)>0: 34 | self.list.SetSelection(0) 35 | self.on_list_change(None) 36 | if self.user==None: 37 | self.new = wx.Button(self.panel, wx.ID_DEFAULT, "&New list") 38 | self.new.Bind(wx.EVT_BUTTON, self.New) 39 | self.main_box.Add(self.new, 0, wx.ALL, 10) 40 | self.edit = wx.Button(self.panel, wx.ID_DEFAULT, "&Edit list") 41 | self.edit.Bind(wx.EVT_BUTTON, self.Edit) 42 | self.main_box.Add(self.edit, 0, wx.ALL, 10) 43 | if len(self.lists)==0: 44 | self.edit.Enable(False) 45 | self.view_members = wx.Button(self.panel, wx.ID_DEFAULT, "&View list members") 46 | self.view_members.Bind(wx.EVT_BUTTON, self.ViewMembers) 47 | self.main_box.Add(self.view_members, 0, wx.ALL, 10) 48 | if len(self.lists)==0 or self.lists[self.list.GetSelection()].member_count==0: 49 | self.view_members.Enable(False) 50 | self.view_subscribers = wx.Button(self.panel, wx.ID_DEFAULT, "&View list subscribers") 51 | self.view_subscribers.Bind(wx.EVT_BUTTON, self.ViewSubscribers) 52 | self.main_box.Add(self.view_subscribers, 0, wx.ALL, 10) 53 | if len(self.lists)==0 or self.lists[self.list.GetSelection()].subscriber_count==0: 54 | self.view_subscribers.Enable(False) 55 | self.remove = wx.Button(self.panel, wx.ID_DEFAULT, "&Remove list") 56 | self.remove.Bind(wx.EVT_BUTTON, self.Remove) 57 | self.main_box.Add(self.remove, 0, wx.ALL, 10) 58 | if len(self.lists)==0: 59 | self.remove.Enable(False) 60 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 61 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 62 | self.main_box.Add(self.close, 0, wx.ALL, 10) 63 | self.panel.Layout() 64 | 65 | def add_items(self): 66 | for i in self.lists: 67 | self.list.Insert(i.name+", "+i.description+", "+str(i.member_count)+" members, "+str(i.subscriber_count)+" subscribers.",self.list.GetCount()) 68 | if len(self.lists)>0: 69 | self.list.SetSelection(0) 70 | else: 71 | if hasattr(self,"load"): 72 | self.load.Enable(False) 73 | if hasattr(self,"edit"): 74 | self.edit.Enable(False) 75 | if hasattr(self,"remove"): 76 | self.remove.Enable(False) 77 | 78 | def on_list_change(self,event): 79 | self.load.Enable(True) 80 | if hasattr(self,"edit"): 81 | self.edit.Enable(True) 82 | if hasattr(self,"remove"): 83 | self.remove.Enable(True) 84 | if hasattr(self,"view_members"): 85 | if len(self.lists)==0 or self.lists[self.list.GetSelection()].member_count==0: 86 | self.view_members.Enable(False) 87 | else: 88 | self.view_members.Enable(True) 89 | if hasattr(self,"view_subscribers"): 90 | if len(self.lists)==0 or self.lists[self.list.GetSelection()].subscriber_count==0: 91 | self.view_subscribers.Enable(False) 92 | else: 93 | self.view_subscribers.Enable(True) 94 | 95 | def New(self, event): 96 | l=NewListGui(self.account) 97 | l.Show() 98 | 99 | def Edit(self, event): 100 | l=NewListGui(self.account,self.lists[self.list.GetSelection()]) 101 | l.Show() 102 | 103 | def Remove(self, event): 104 | self.account.api.destroy_list(list_id=self.lists[self.list.GetSelection()].id) 105 | self.lists.remove(self.lists[self.list.GetSelection()]) 106 | self.list.Clear() 107 | self.add_items() 108 | 109 | def ViewSubscribers(self, event): 110 | list=self.lists[self.list.GetSelection()] 111 | v=view.UserViewGui(self.account,self.account.api.get_list_subscribers(count=200, list_id=list.id),"List subscribers") 112 | v.Show() 113 | 114 | def ViewMembers(self, event): 115 | list=self.lists[self.list.GetSelection()] 116 | v=view.UserViewGui(self.account,self.account.api.get_list_members(count=200, list_id=list.id),"List members") 117 | v.Show() 118 | 119 | def Load(self, event): 120 | if self.user==None: 121 | misc.list_timeline(self.account,self.lists[self.list.GetSelection()].name, self.lists[self.list.GetSelection()].id) 122 | else: 123 | if self.add: 124 | self.account.api.add_list_member(user_id=self.user.id, list_id=self.lists[self.list.GetSelection()].id) 125 | else: 126 | self.account.api.remove_list_member(user_id=self.user.id, list_id=self.lists[self.list.GetSelection()].id) 127 | self.Destroy() 128 | 129 | def OnClose(self, event): 130 | self.Destroy() 131 | 132 | class NewListGui(wx.Dialog): 133 | def __init__(self,account,list=None): 134 | self.account=account 135 | self.list=list 136 | title="New list" 137 | if list!=None: 138 | title="Edit list "+list.name 139 | wx.Dialog.__init__(self, None, title=title, size=(350,200)) 140 | self.Bind(wx.EVT_CLOSE, self.OnClose) 141 | self.panel = wx.Panel(self) 142 | self.main_box = wx.BoxSizer(wx.VERTICAL) 143 | self.text_label = wx.StaticText(self.panel, -1, "Name of list") 144 | self.text = wx.TextCtrl(self.panel, -1, "",style=wx.TE_PROCESS_ENTER|wx.TE_DONTWRAP) 145 | self.main_box.Add(self.text, 0, wx.ALL, 10) 146 | self.text.SetFocus() 147 | if list!=None: 148 | self.text.SetValue(self.list.name) 149 | self.text2_label = wx.StaticText(self.panel, -1, "Description of list") 150 | self.text2 = wx.TextCtrl(self.panel, -1, "",style=wx.TE_PROCESS_ENTER|wx.TE_DONTWRAP) 151 | self.main_box.Add(self.text2, 0, wx.ALL, 10) 152 | if list!=None: 153 | self.text2.SetValue(self.list.description) 154 | self.type_label = wx.StaticText(self.panel, -1, "Mode") 155 | self.type = wx.ComboBox(self.panel, -1, "",style=wx.CB_READONLY) 156 | self.type.Insert("private",0) 157 | self.type.Insert("public",1) 158 | self.type.SetSelection(0) 159 | if self.list!=None: 160 | if self.list.mode=="public": 161 | self.type.SetSelection(1) 162 | 163 | self.main_box.Add(self.type, 0, wx.ALL, 10) 164 | if self.list!=None: 165 | self.create = wx.Button(self.panel, wx.ID_DEFAULT, "&Edit list") 166 | else: 167 | self.create = wx.Button(self.panel, wx.ID_DEFAULT, "&Create list") 168 | self.create.SetDefault() 169 | self.create.Bind(wx.EVT_BUTTON, self.Create) 170 | self.main_box.Add(self.create, 0, wx.ALL, 10) 171 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 172 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 173 | self.main_box.Add(self.close, 0, wx.ALL, 10) 174 | self.panel.Layout() 175 | 176 | def Create(self, event): 177 | if self.list==None: 178 | self.account.api.create_list(name=self.text.GetValue(),mode=self.type.GetString(self.type.GetSelection()),description=self.text2.GetValue()) 179 | else: 180 | self.account.api.update_list(list_id=self.list.id, name=self.text.GetValue(),mode=self.type.GetString(self.type.GetSelection()),description=self.text2.GetValue()) 181 | self.Destroy() 182 | 183 | def OnClose(self, event): 184 | self.Destroy() -------------------------------------------------------------------------------- /GUI/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import webbrowser 3 | import platform 4 | import pyperclip 5 | import sys 6 | import application 7 | import wx 8 | from keyboard_handler.wx_handler import WXKeyboardHandler 9 | import globals 10 | import speak 11 | from . import account_options, accounts, chooser, invisible, lists, misc, options, profile, search, timelines, tray, tweet, view 12 | import utils 13 | import sound 14 | import timeline 15 | import threading 16 | 17 | class MainGui(wx.Frame): 18 | def __init__(self, title): 19 | self.invisible=False 20 | wx.Frame.__init__(self, None, title=title,size=(800,600)) 21 | self.Center() 22 | if platform.system()!="Darwin": 23 | self.trayicon=tray.TaskBarIcon(self) 24 | self.handler=WXKeyboardHandler(self) 25 | self.handler.register_key("control+win+shift+t",self.ToggleWindow) 26 | self.handler.register_key("alt+win+shift+q",self.OnClose) 27 | self.Bind(wx.EVT_CLOSE, self.OnClose) 28 | self.panel = wx.Panel(self) 29 | self.main_box = wx.BoxSizer(wx.VERTICAL) 30 | self.menuBar = wx.MenuBar() 31 | if platform.system()!="Darwin": 32 | ctrl="control" 33 | else: 34 | ctrl="command" 35 | 36 | menu = wx.Menu() 37 | m_accounts = menu.Append(-1, "Accounts ("+ctrl+"+A)", "accounts") 38 | self.Bind(wx.EVT_MENU, self.OnAccounts, m_accounts) 39 | m_update_profile = menu.Append(-1, "Update profile", "profile") 40 | self.Bind(wx.EVT_MENU, self.OnUpdateProfile, m_update_profile) 41 | m_lists = menu.Append(-1, "Lists", "lists") 42 | self.Bind(wx.EVT_MENU, self.OnLists, m_lists) 43 | m_followers = menu.Append(-1, "List Followers ("+ctrl+"+Left Bracket)", "followers") 44 | self.Bind(wx.EVT_MENU, self.OnFollowers, m_followers) 45 | m_friends = menu.Append(-1, "List Friends ("+ctrl+"+right bracket)", "friends") 46 | self.Bind(wx.EVT_MENU, self.OnFriends, m_friends) 47 | if platform.system()!="Darwin": 48 | m_options = menu.Append(wx.ID_PREFERENCES, "Global Options", "options") 49 | else: 50 | m_options = menu.Append(wx.ID_PREFERENCES, "Preferences ("+ctrl+"+comma", "options") 51 | self.Bind(wx.EVT_MENU, self.OnOptions, m_options) 52 | m_account_options = menu.Append(-1, "Account options", "account_options") 53 | self.Bind(wx.EVT_MENU, self.OnAccountOptions, m_account_options) 54 | m_close = menu.Append(wx.ID_EXIT, "exit", "exit") 55 | self.Bind(wx.EVT_MENU, self.OnClose, m_close) 56 | self.menuBar.Append(menu, "&Application") 57 | menu2 = wx.Menu() 58 | m_tweet = menu2.Append(-1, "New tweet ("+ctrl+"+n)", "tweet") 59 | self.Bind(wx.EVT_MENU, self.OnTweet, m_tweet) 60 | m_reply = menu2.Append(-1, "Reply ("+ctrl+"+r)", "reply") 61 | self.Bind(wx.EVT_MENU, self.OnReply, m_reply) 62 | m_retweet = menu2.Append(-1, "Retweet ("+ctrl+"+shift+r)", "retweet") 63 | self.Bind(wx.EVT_MENU, self.OnRetweet, m_retweet) 64 | if platform.system()=="Darwin": 65 | m_quote = menu2.Append(-1, "Quote (option+q)", "quote") 66 | else: 67 | m_quote = menu2.Append(-1, "Quote ("+ctrl+"+q)", "quote") 68 | self.Bind(wx.EVT_MENU, self.OnQuote, m_quote) 69 | m_like=menu2.Append(-1, "Like ("+ctrl+"+l)", "like") 70 | self.Bind(wx.EVT_MENU, self.OnLike, m_like) 71 | m_url=menu2.Append(-1, "Open URL ("+ctrl+"+o)", "url") 72 | self.Bind(wx.EVT_MENU, self.OnUrl, m_url) 73 | m_tweet_url=menu2.Append(-1, "Open URL of Tweet ("+ctrl+"+shift+o)", "tweet_url") 74 | self.Bind(wx.EVT_MENU, self.OnTweetUrl, m_tweet_url) 75 | m_delete = menu2.Append(-1, "Delete Tweet (Delete)", "tweet") 76 | self.Bind(wx.EVT_MENU, self.OnDelete, m_delete) 77 | m_copy = menu2.Append(-1, "Copy tweet to clipboard ("+ctrl+"+c)", "copy") 78 | self.Bind(wx.EVT_MENU, self.onCopy, m_copy) 79 | m_message=menu2.Append(-1, "Send message ("+ctrl+"+d)", "message") 80 | self.Bind(wx.EVT_MENU, self.OnMessage, m_message) 81 | m_follow=menu2.Append(-1, "Follow ("+ctrl+"+f)", "follow") 82 | self.Bind(wx.EVT_MENU, self.OnFollow, m_follow) 83 | m_unfollow=menu2.Append(-1, "Unfollow ("+ctrl+"+shift+f", "follow") 84 | self.Bind(wx.EVT_MENU, self.OnUnfollow, m_unfollow) 85 | m_add_to_list=menu2.Append(-1, "Add to list ("+ctrl+"+i)", "addlist") 86 | self.Bind(wx.EVT_MENU, self.OnAddToList, m_add_to_list) 87 | m_remove_from_list=menu2.Append(-1, "Remove from list ("+ctrl+"+shift+i)", "removelist") 88 | self.Bind(wx.EVT_MENU, self.OnRemoveFromList, m_remove_from_list) 89 | m_block=menu2.Append(-1, "Block ("+ctrl+"+b)", "block") 90 | self.Bind(wx.EVT_MENU, self.OnBlock, m_block) 91 | m_unblock=menu2.Append(-1, "Unblock ("+ctrl+"+shift+b)", "unblock") 92 | self.Bind(wx.EVT_MENU, self.OnUnblock, m_unblock) 93 | m_mute_user=menu2.Append(-1, "Mute", "mute") 94 | self.Bind(wx.EVT_MENU, self.OnMuteUser, m_mute_user) 95 | m_unmute_user=menu2.Append(-1, "Unmute", "unmute") 96 | self.Bind(wx.EVT_MENU, self.OnUnmuteUser, m_unmute_user) 97 | m_view=menu2.Append(-1, "View tweet (Enter)", "view") 98 | self.Bind(wx.EVT_MENU, self.OnView, m_view) 99 | m_user_profile=menu2.Append(-1, "User Profile ("+ctrl+"+shift+u)", "profile") 100 | self.Bind(wx.EVT_MENU, self.OnUserProfile, m_user_profile) 101 | m_speak_user=menu2.Append(-1, "Speak user ("+ctrl+"+semicolon)", "speak") 102 | self.Bind(wx.EVT_MENU, self.OnSpeakUser, m_speak_user) 103 | m_speak_reply=menu2.Append(-1, "Speak reference tweet of this reply ("+ctrl+"+shift+semicolon)", "speak2") 104 | self.Bind(wx.EVT_MENU, self.OnSpeakReply, m_speak_reply) 105 | m_conversation=menu2.Append(-1, "Load conversation/related tweets ("+ctrl+"+g)", "conversation") 106 | self.Bind(wx.EVT_MENU, self.OnConversation, m_conversation) 107 | self.menuBar.Append(menu2, "A&ctions") 108 | menu7 = wx.Menu() 109 | m_mutual_following=menu7.Append(-1, "View mutual follows (users who I follow that also follow me)", "conversation") 110 | self.Bind(wx.EVT_MENU, self.OnMutualFollowing, m_mutual_following) 111 | m_not_following=menu7.Append(-1, "View users who follow me that I do not follow", "conversation") 112 | self.Bind(wx.EVT_MENU, self.OnNotFollowing, m_not_following) 113 | m_not_following_me=menu7.Append(-1, "View users who I follow that do not follow me", "conversation") 114 | self.Bind(wx.EVT_MENU, self.OnNotFollowingMe, m_not_following_me) 115 | m_havent_tweeted=menu7.Append(-1, "View users who I follow that haven't tweeted in a year", "conversation") 116 | self.Bind(wx.EVT_MENU, self.OnHaventTweeted, m_havent_tweeted) 117 | self.menuBar.Append(menu7, "U&sers") 118 | menu3 = wx.Menu() 119 | m_refresh = menu3.Append(-1, "Refresh timeline (F5)", "refresh") 120 | self.Bind(wx.EVT_MENU, self.onRefresh, m_refresh) 121 | m_prev = menu3.Append(-1, "Load older tweets (alt/option+pageup)", "prev") 122 | self.Bind(wx.EVT_MENU, self.onPrev, m_prev) 123 | m_hide = menu3.Append(-1, "Hide Timeline ("+ctrl+"+h)", "hide") 124 | self.Bind(wx.EVT_MENU, self.OnHide, m_hide) 125 | m_manage_hide = menu3.Append(-1, "Manage hidden Timelines ("+ctrl+"+shift+h)", "manage_hide") 126 | self.Bind(wx.EVT_MENU, self.OnManageHide, m_manage_hide) 127 | m_read = menu3.Append(-1, "Toggle autoread ("+ctrl+"+e)", "autoread") 128 | self.Bind(wx.EVT_MENU, self.OnRead, m_read) 129 | if platform.system()!="Darwin": 130 | m_mute = menu3.Append(-1, "Toggle mute ("+ctrl+"+m)", "mute") 131 | else: 132 | m_mute = menu3.Append(-1, "Toggle mute ("+ctrl+"+shift+m)", "mute") 133 | self.Bind(wx.EVT_MENU, self.OnMute, m_mute) 134 | m_user_timeline = menu3.Append(-1, "User timeline ("+ctrl+"+u)", "user") 135 | self.Bind(wx.EVT_MENU, self.OnUserTimeline, m_user_timeline) 136 | m_search = menu3.Append(-1, "Search ("+ctrl+"+slash)", "search") 137 | self.Bind(wx.EVT_MENU, self.OnSearch, m_search) 138 | m_user_search = menu3.Append(-1, "User Search ("+ctrl+"+shift+slash)", "search") 139 | self.Bind(wx.EVT_MENU, self.OnUserSearch, m_user_search) 140 | self.m_close_timeline = menu3.Append(-1, "Close timeline ("+ctrl+"+w)", "removetimeline") 141 | self.m_close_timeline.Enable(False) 142 | self.Bind(wx.EVT_MENU, self.OnCloseTimeline, self.m_close_timeline) 143 | self.menuBar.Append(menu3, "Time&line") 144 | menu4 = wx.Menu() 145 | m_play_external = menu4.Append(-1, "Play media ("+ctrl+"+enter)", "play_external") 146 | self.Bind(wx.EVT_MENU, self.OnPlayExternal, m_play_external) 147 | m_volup = menu4.Append(-1, "Volume up (alt/option+up)", "volup") 148 | self.Bind(wx.EVT_MENU, self.OnVolup, m_volup) 149 | m_voldown = menu4.Append(-1, "Volume down (alt/option+down)", "voldown") 150 | self.Bind(wx.EVT_MENU, self.OnVoldown, m_voldown) 151 | self.menuBar.Append(menu4, "A&udio") 152 | menu5 = wx.Menu() 153 | m_previous_in_thread = menu5.Append(-1, "Previous tweet in thread ("+ctrl+"+up)", "prevtweet") 154 | self.Bind(wx.EVT_MENU, self.OnPreviousInThread, m_previous_in_thread) 155 | m_next_in_thread = menu5.Append(-1, "Next tweet in thread ("+ctrl+"+down)", "nexttweet") 156 | self.Bind(wx.EVT_MENU, self.OnNextInThread, m_next_in_thread) 157 | m_previous_from_user = menu5.Append(-1, "Previous tweet from user ("+ctrl+"+left)", "prevuser") 158 | self.Bind(wx.EVT_MENU, self.OnPreviousFromUser, m_previous_from_user) 159 | m_next_from_user = menu5.Append(-1, "Next tweet from user ("+ctrl+"+right)", "nextuser") 160 | self.Bind(wx.EVT_MENU, self.OnNextFromUser, m_next_from_user) 161 | m_next_timeline = menu5.Append(-1, "Next timeline (alt/option+right)", "nexttl") 162 | self.Bind(wx.EVT_MENU, self.OnNextTimeline, m_next_timeline) 163 | m_prev_timeline = menu5.Append(-1, "Previous timeline (alt/Option+left)", "prevtl") 164 | self.Bind(wx.EVT_MENU, self.OnPrevTimeline, m_prev_timeline) 165 | self.menuBar.Append(menu5, "Navigation") 166 | menu6 = wx.Menu() 167 | m_readme = menu6.Append(-1, "Readme (F1)", "readme") 168 | self.Bind(wx.EVT_MENU, self.OnReadme, m_readme) 169 | m_cfu = menu6.Append(-1, "Check for updates", "cfu") 170 | self.Bind(wx.EVT_MENU, self.OnCfu, m_cfu) 171 | if platform.system()=="Windows": 172 | m_download_QPlay = menu6.Append(-1, "Redownload QPlay", "download_QPlay") 173 | self.Bind(wx.EVT_MENU, self.OnDownloadQPlay, m_download_QPlay) 174 | m_stats = menu6.Append(-1, "Stats for nerds", "stats") 175 | self.Bind(wx.EVT_MENU, self.OnStats, m_stats) 176 | m_errors = menu6.Append(-1, "View API errors", "errors") 177 | self.Bind(wx.EVT_MENU, self.OnErrors, m_errors) 178 | m_view_user_db = menu6.Append(-1, "View user database", "viewusers") 179 | self.Bind(wx.EVT_MENU, self.OnViewUserDb, m_view_user_db) 180 | m_clean_user_db = menu6.Append(-1, "Refresh user database", "cleanusers") 181 | self.Bind(wx.EVT_MENU, self.OnCleanUserDb, m_clean_user_db) 182 | self.menuBar.Append(menu6, "&Help") 183 | self.SetMenuBar(self.menuBar) 184 | self.list_label=wx.StaticText(self.panel, -1, label="Timelines") 185 | self.list=wx.ListBox(self.panel, -1) 186 | self.main_box.Add(self.list, 0, wx.ALL, 10) 187 | self.list.Bind(wx.EVT_LISTBOX, self.on_list_change) 188 | self.list.SetFocus() 189 | self.list2_label=wx.StaticText(self.panel, -1, label="Contents") 190 | self.list2=wx.ListBox(self.panel, -1,size=(1200,800)) 191 | self.main_box.Add(self.list2, 0, wx.ALL, 10) 192 | self.list2.Bind(wx.EVT_LISTBOX, self.on_list2_change) 193 | accel=[] 194 | accel.append((wx.ACCEL_ALT, ord('X'), m_close.GetId())) 195 | if platform.system()=="Darwin": 196 | accel.append((wx.ACCEL_CTRL, ord(','), m_options.GetId())) 197 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord(','), m_account_options.GetId())) 198 | accel.append((wx.ACCEL_CTRL, ord('N'), m_tweet.GetId())) 199 | accel.append((wx.ACCEL_CTRL, ord('R'), m_reply.GetId())) 200 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('R'), m_retweet.GetId())) 201 | if platform.system()=="Darwin": 202 | accel.append((wx.ACCEL_ALT, ord('Q'), m_quote.GetId())) 203 | else: 204 | accel.append((wx.ACCEL_CTRL, ord('Q'), m_quote.GetId())) 205 | accel.append((wx.ACCEL_CTRL, ord('O'), m_url.GetId())) 206 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('O'), m_tweet_url.GetId())) 207 | accel.append((wx.ACCEL_CTRL, ord('D'), m_message.GetId())) 208 | accel.append((wx.ACCEL_CTRL, ord('f'), m_follow.GetId())) 209 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('f'), m_unfollow.GetId())) 210 | accel.append((wx.ACCEL_CTRL, ord('b'), m_block.GetId())) 211 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('b'), m_unblock.GetId())) 212 | accel.append((wx.ACCEL_CTRL, ord('a'), m_accounts.GetId())) 213 | accel.append((wx.ACCEL_CTRL, ord('i'), m_add_to_list.GetId())) 214 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('i'), m_remove_from_list.GetId())) 215 | accel.append((wx.ACCEL_CTRL, ord("c"), m_copy.GetId())) 216 | accel.append((wx.ACCEL_NORMAL, wx.WXK_RETURN, m_view.GetId())) 217 | accel.append((wx.ACCEL_CTRL, wx.WXK_RETURN, m_play_external.GetId())) 218 | accel.append((wx.ACCEL_CTRL, ord('U'), m_user_timeline.GetId())) 219 | accel.append((wx.ACCEL_CTRL, ord('/'), m_search.GetId())) 220 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('/'), m_user_search.GetId())) 221 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('U'), m_user_profile.GetId())) 222 | accel.append((wx.ACCEL_CTRL, ord('W'), self.m_close_timeline.GetId())) 223 | accel.append((wx.ACCEL_ALT, wx.WXK_PAGEUP, m_prev.GetId())) 224 | accel.append((wx.ACCEL_CTRL, ord('h'), m_hide.GetId())) 225 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('h'), m_manage_hide.GetId())) 226 | accel.append((wx.ACCEL_CTRL, ord('L'), m_like.GetId())) 227 | accel.append((wx.ACCEL_CTRL, ord('G'), m_conversation.GetId())) 228 | accel.append((wx.ACCEL_CTRL, ord(';'), m_speak_user.GetId())) 229 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord(';'), m_speak_reply.GetId())) 230 | accel.append((wx.ACCEL_ALT, wx.WXK_UP, m_volup.GetId())) 231 | accel.append((wx.ACCEL_ALT, wx.WXK_DOWN, m_voldown.GetId())) 232 | accel.append((wx.ACCEL_CTRL, ord("e"), m_read.GetId())) 233 | if platform.system()=="Darwin": 234 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord("m"), m_mute.GetId())) 235 | else: 236 | accel.append((wx.ACCEL_CTRL, ord("m"), m_mute.GetId())) 237 | accel.append((wx.ACCEL_CTRL, wx.WXK_UP, m_previous_in_thread.GetId())) 238 | accel.append((wx.ACCEL_CTRL, wx.WXK_DOWN, m_next_in_thread.GetId())) 239 | accel.append((wx.ACCEL_CTRL, wx.WXK_LEFT, m_previous_from_user.GetId())) 240 | accel.append((wx.ACCEL_CTRL, wx.WXK_RIGHT, m_next_from_user.GetId())) 241 | accel.append((wx.ACCEL_ALT, wx.WXK_RIGHT, m_next_timeline.GetId())) 242 | accel.append((wx.ACCEL_ALT, wx.WXK_LEFT, m_prev_timeline.GetId())) 243 | accel.append((wx.ACCEL_CTRL, ord("["), m_followers.GetId())) 244 | accel.append((wx.ACCEL_CTRL, ord("]"), m_friends.GetId())) 245 | accel.append((wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord("L"), m_lists.GetId())) 246 | accel.append((wx.ACCEL_NORMAL, wx.WXK_F5, m_refresh.GetId())) 247 | accel.append((wx.ACCEL_NORMAL, wx.WXK_DELETE, m_delete.GetId())) 248 | accel.append((wx.ACCEL_NORMAL, wx.WXK_F1, m_readme.GetId())) 249 | accel_tbl=wx.AcceleratorTable(accel) 250 | self.SetAcceleratorTable(accel_tbl) 251 | self.panel.Layout() 252 | 253 | def register_keys(self): 254 | self.invisible=True 255 | if platform.system()=="Darwin": 256 | f=open("keymac.keymap","r") 257 | else: 258 | f=open("keymap.keymap","r") 259 | keys=f.read().split("\n") 260 | f.close() 261 | for i in keys: 262 | key=i.strip(" ").split("=") 263 | success=invisible.register_key(key[0],key[1]) 264 | 265 | def unregister_keys(self): 266 | self.invisible=False 267 | f=open("keymap.keymap","r") 268 | keys=f.read().split("\n") 269 | f.close() 270 | for i in keys: 271 | key=i.split("=") 272 | success=invisible.register_key(key[0],key[1],False) 273 | 274 | def ToggleWindow(self): 275 | if self.IsShown(): 276 | self.Show(False) 277 | globals.prefs.window_shown=False 278 | else: 279 | self.Show(True) 280 | self.Raise() 281 | globals.prefs.window_shown=True 282 | if not globals.prefs.invisible_sync: 283 | self.list.SetSelection(globals.currentAccount.currentIndex) 284 | self.on_list_change(None) 285 | self.list2.SetSelection(globals.currentAccount.currentTimeline.index) 286 | self.on_list2_change(None) 287 | 288 | def OnReadme(self,event=None): 289 | webbrowser.open("http://quinterApp.github.io/readme.html") 290 | 291 | def OnRead(self,event=None): 292 | globals.currentAccount.currentTimeline.toggle_read() 293 | 294 | def OnMute(self,event=None): 295 | globals.currentAccount.currentTimeline.toggle_mute() 296 | 297 | def OnStats(self, event=None): 298 | txt=view.ViewTextGui("You have sent a total of "+str(globals.prefs.tweets_sent)+" tweets, of which "+str(globals.prefs.replies_sent)+" are replies and "+str(globals.prefs.quotes_sent)+" are quotes.\r\nYou have retweeted "+str(globals.prefs.retweets_sent)+" tweets, and liked "+str(globals.prefs.likes_sent)+" tweets.\r\nYou have sent "+str(globals.prefs.chars_sent)+" characters to Twitter from Quinter!\r\nYou have received "+str(globals.prefs.statuses_received)+" tweets in total through all of your timelines.") 299 | txt.Show() 300 | 301 | def OnErrors(self, event=None): 302 | errors="" 303 | for i in globals.errors: 304 | errors+=i+"\r\n" 305 | txt=view.ViewTextGui(errors) 306 | txt.Show() 307 | 308 | def OnManageHide(self, event=None): 309 | gui=timelines.HiddenTimelinesGui(globals.currentAccount) 310 | gui.Show() 311 | 312 | def OnDownloadQPlay(self, event=None): 313 | threading.Thread(target=utils.download_QPlay).start() 314 | 315 | def OnCfu(self, event=None): 316 | utils.cfu(False) 317 | 318 | def onCopy(self,event=None): 319 | pyperclip.copy(utils.template_to_string(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index],globals.prefs.copyTemplate)) 320 | speak.speak("Copied") 321 | 322 | def OnClose(self, event=None): 323 | speak.speak("Exiting.") 324 | if platform.system()!="Darwin": 325 | self.trayicon.on_exit(event,False) 326 | self.Destroy() 327 | sys.exit() 328 | 329 | def OnPlayExternal(self,event=None): 330 | thread=threading.Thread(target=misc.play_external,args=(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index],)).start() 331 | 332 | 333 | def OnConversation(self,event=None): 334 | misc.load_conversation(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 335 | 336 | def OnDelete(self,event=None): 337 | misc.delete(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 338 | 339 | def OnHide(self,event=None): 340 | globals.currentAccount.currentTimeline.hide_tl() 341 | 342 | def OnNextInThread(self,event=None): 343 | if not globals.prefs.reversed: 344 | misc.next_in_thread(globals.currentAccount) 345 | else: 346 | misc.previous_in_thread(globals.currentAccount) 347 | 348 | def OnPreviousInThread(self,event=None): 349 | if not globals.prefs.reversed: 350 | misc.previous_in_thread(globals.currentAccount) 351 | else: 352 | misc.next_in_thread(globals.currentAccount) 353 | 354 | def OnPreviousFromUser(self,event=None): 355 | misc.previous_from_user(globals.currentAccount) 356 | 357 | def OnNextTimeline(self,event=None): 358 | invisible.inv.next_tl(True) 359 | 360 | def OnPrevTimeline(self,event=None): 361 | invisible.inv.prev_tl(True) 362 | 363 | def OnNextFromUser(self,event=None): 364 | misc.next_from_user(globals.currentAccount) 365 | 366 | def OnSpeakUser(self,event=None): 367 | users=[] 368 | if globals.currentAccount.currentTimeline.type=="messages": 369 | users.append(utils.lookup_user(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index].message_create["sender_id"]).screen_name) 370 | else: 371 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 372 | users.append(status.user.screen_name) 373 | if hasattr(status,"quoted_status") and status.quoted_status.user.screen_name not in users: 374 | users.insert(0,status.quoted_status.user.screen_name) 375 | if hasattr(status,"retweeted_status") and status.retweeted_status.user.screen_name not in users: 376 | users.insert(0,status.retweeted_status.user.screen_name) 377 | for i in utils.get_user_objects_in_tweet(globals.currentAccount,status): 378 | if i.screen_name not in users: 379 | users.append(i.screen_name) 380 | utils.speak_user(globals.currentAccount,users) 381 | 382 | def OnSpeakReply(self,event=None): 383 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 384 | utils.speak_reply(globals.currentAccount,status) 385 | 386 | def refreshTimelines(self): 387 | old_selection=self.list.GetSelection() 388 | self.list.Clear() 389 | for i in globals.currentAccount.list_timelines(): 390 | self.list.Insert(i.name,self.list.GetCount()) 391 | try: 392 | self.list.SetSelection(old_selection) 393 | except: 394 | self.list.SetSelection(1) 395 | 396 | def on_list_change(self, event): 397 | globals.currentAccount.currentTimeline=globals.currentAccount.list_timelines()[self.list.GetSelection()] 398 | globals.currentAccount.currentIndex=self.list.GetSelection() 399 | if globals.currentAccount.currentTimeline.removable: 400 | self.m_close_timeline.Enable(True) 401 | else: 402 | self.m_close_timeline.Enable(False) 403 | 404 | self.play_earcon() 405 | self.refreshList() 406 | 407 | def play_earcon(self): 408 | if globals.prefs.earcon_top and (not globals.prefs.reversed and globals.currentAccount.currentTimeline.index > 0 or globals.prefs.reversed and globals.currentAccount.currentTimeline.index < len(globals.currentAccount.currentTimeline.statuses) - 1): 409 | sound.play(globals.currentAccount,"new") 410 | 411 | def OnFollowers(self,event=None): 412 | misc.followers(globals.currentAccount) 413 | 414 | def OnFriends(self,event=None): 415 | misc.friends(globals.currentAccount) 416 | 417 | def OnMutualFollowing(self,event=None): 418 | misc.mutual_following(globals.currentAccount) 419 | 420 | def OnNotFollowing(self,event=None): 421 | misc.not_following(globals.currentAccount) 422 | 423 | def OnNotFollowingMe(self,event=None): 424 | misc.not_following_me(globals.currentAccount) 425 | 426 | def OnHaventTweeted(self,event=None): 427 | misc.havent_tweeted(globals.currentAccount) 428 | 429 | def refreshList(self): 430 | stuffage=globals.currentAccount.currentTimeline.get() 431 | self.list2.Freeze() 432 | self.list2.Clear() 433 | for i in stuffage: 434 | self.list2.Insert(i,self.list2.GetCount()) 435 | try: 436 | self.list2.SetSelection(globals.currentAccount.currentTimeline.index) 437 | except: 438 | self.list2.SetSelection(globals.currentAccount.currentTimeline.index-1) 439 | self.list2.Thaw() 440 | 441 | def OnViewUserDb(self, event=None): 442 | u=view.UserViewGui(globals.currentAccount,globals.users,"User Database containing "+str(len(globals.users))+" users.") 443 | u.Show() 444 | 445 | def OnCleanUserDb(self, event=None): 446 | globals.clean_users() 447 | globals.save_users() 448 | 449 | def on_list2_change(self, event): 450 | globals.currentAccount.currentTimeline.index=self.list2.GetSelection() 451 | if globals.prefs.earcon_audio and len(sound.get_media_urls(utils.find_urls_in_tweet(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]))) > 0: 452 | sound.play(globals.currentAccount,"media") 453 | 454 | def onRefresh(self,event=None): 455 | globals.currentAccount.currentTimeline.load() 456 | 457 | def add_to_list(self,list): 458 | self.list2.Freeze() 459 | for i in list: 460 | self.list2.Insert(i,0) 461 | self.list2.Thaw() 462 | 463 | def append_to_list(self,list): 464 | self.list2.Freeze() 465 | for i in list: 466 | self.list2.Insert(i,self.list2.GetCount()) 467 | self.list2.Thaw() 468 | 469 | def OnView(self,event=None): 470 | viewer=view.ViewGui(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 471 | viewer.Show() 472 | 473 | def onPrev(self,event=None): 474 | globals.currentAccount.currentTimeline.load(True) 475 | 476 | def OnVolup(self, event=None): 477 | if globals.prefs.volume<1.0: 478 | globals.prefs.volume+=0.1 479 | globals.prefs.volume=round(globals.prefs.volume,1) 480 | sound.play(globals.currentAccount,"volume_changed") 481 | 482 | def OnVoldown(self, event=None): 483 | if globals.prefs.volume>0.0: 484 | globals.prefs.volume-=0.1 485 | globals.prefs.volume=round(globals.prefs.volume,1) 486 | sound.play(globals.currentAccount,"volume_changed") 487 | 488 | def OnOptions(self, event=None): 489 | Opt=options.OptionsGui() 490 | Opt.Show() 491 | 492 | def OnAccountOptions(self, event=None): 493 | Opt=account_options.OptionsGui(globals.currentAccount) 494 | Opt.Show() 495 | 496 | def OnUpdateProfile(self, event=None): 497 | Profile=profile.ProfileGui(globals.currentAccount) 498 | Profile.Show() 499 | 500 | def OnAccounts(self, event=None): 501 | acc=accounts.AccountsGui() 502 | acc.Show() 503 | 504 | def OnTweet(self, event=None): 505 | NewTweet=tweet.TweetGui(globals.currentAccount) 506 | NewTweet.Show() 507 | 508 | def OnUserTimeline(self, event=None): 509 | misc.user_timeline(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 510 | 511 | def OnSearch(self, event=None): 512 | s=search.SearchGui(globals.currentAccount) 513 | s.Show() 514 | 515 | def OnUserSearch(self, event=None): 516 | s=search.SearchGui(globals.currentAccount,"user") 517 | s.Show() 518 | 519 | def OnLists(self, event=None): 520 | s=lists.ListsGui(globals.currentAccount) 521 | s.Show() 522 | 523 | def OnUserProfile(self, event=None): 524 | misc.user_profile(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 525 | 526 | def OnUrl(self, event=None): 527 | misc.url_chooser(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 528 | 529 | def OnTweetUrl(self, event=None): 530 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 531 | if platform.system()!="Darwin": 532 | webbrowser.open("https://twitter.com/"+status.user.screen_name+"/status/"+str(status.id)) 533 | else: 534 | os.system("open https://twitter.com/"+status.user.screen_name+"/status/"+str(status.id)) 535 | 536 | def OnFollow(self, event=None): 537 | misc.follow(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 538 | 539 | def OnAddToList(self, event=None): 540 | misc.add_to_list(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 541 | 542 | def OnRemoveFromList(self, event=None): 543 | misc.remove_from_list(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 544 | 545 | def OnUnfollow(self, event=None): 546 | misc.unfollow(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 547 | 548 | def OnBlock(self, event=None): 549 | misc.block(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 550 | 551 | def OnUnblock(self, event=None): 552 | misc.unblock(globals.currentAccount, globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 553 | 554 | def OnMuteUser(self, event=None): 555 | misc.mute(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 556 | 557 | def OnUnmuteUser(self, event=None): 558 | misc.unmute(globals.currentAccount,globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]) 559 | 560 | def OnCloseTimeline(self, event=None): 561 | tl=globals.currentAccount.currentTimeline 562 | if tl.removable: 563 | if globals.prefs.ask_dismiss: 564 | dlg=wx.MessageDialog(None,"Are you sure you wish to close "+tl.name+"?","Warning",wx.YES_NO | wx.ICON_QUESTION) 565 | result=dlg.ShowModal() 566 | dlg.Destroy() 567 | if not globals.prefs.ask_dismiss or globals.prefs.ask_dismiss and result== wx.ID_YES: 568 | if tl.type=="user" and tl.data in globals.currentAccount.prefs.user_timelines: 569 | globals.currentAccount.prefs.user_timelines.remove(tl.data) 570 | if tl.type=="list" and tl.data in globals.currentAccount.prefs.list_timelines: 571 | globals.currentAccount.prefs.list_timelines.remove(tl.data) 572 | if tl.type=="search" and tl.data in globals.currentAccount.prefs.search_timelines: 573 | globals.currentAccount.prefs.search_timelines.remove(tl.data) 574 | globals.currentAccount.timelines.remove(tl) 575 | sound.play(globals.currentAccount,"close") 576 | self.refreshTimelines() 577 | self.list.SetSelection(0) 578 | globals.currentAccount.currentIndex=0 579 | self.on_list_change(None) 580 | del tl 581 | 582 | def OnReply(self, event=None): 583 | if globals.currentAccount.currentTimeline.type=="messages": 584 | self.OnMessage(None) 585 | else: 586 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 587 | misc.reply(globals.currentAccount,status) 588 | 589 | def OnQuote(self, event=None): 590 | if globals.currentAccount.currentTimeline.type=="messages": 591 | self.OnMessage(None) 592 | else: 593 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 594 | misc.quote(globals.currentAccount,status) 595 | 596 | def OnMessage(self, event=None): 597 | if globals.currentAccount.currentTimeline.type=="messages": 598 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 599 | else: 600 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 601 | misc.message(globals.currentAccount,status) 602 | 603 | def OnRetweet(self, event=None): 604 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 605 | misc.retweet(globals.currentAccount,status) 606 | 607 | def OnLike(self, event=None): 608 | status=globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index] 609 | misc.like(globals.currentAccount,status) 610 | 611 | global window 612 | window=MainGui(application.name+" "+application.version) 613 | -------------------------------------------------------------------------------- /GUI/misc.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import platform 4 | import subprocess 5 | import speak 6 | import sound 7 | import utils 8 | from . import chooser, main, tweet, view 9 | import timeline 10 | import globals 11 | from tweepy import TweepyException 12 | def reply(account,status): 13 | NewTweet=tweet.TweetGui(account,"",type="reply",status=status) 14 | NewTweet.Show() 15 | 16 | def quote(account,status): 17 | NewTweet=tweet.TweetGui(account,type="quote",status=status) 18 | NewTweet.Show() 19 | 20 | def user_timeline(account,status): 21 | u=utils.get_user_objects_in_tweet(account,status) 22 | u2=[] 23 | for i in u: 24 | u2.append(i.screen_name) 25 | chooser.chooser(account,"User Timeline","Choose user timeline",u2,"userTimeline") 26 | 27 | def user_profile(account,status): 28 | u=utils.get_user_objects_in_tweet(account,status) 29 | u2=[] 30 | for i in u: 31 | u2.append(i.screen_name) 32 | chooser.chooser(account,"User Profile","Choose user profile",u2,"profile") 33 | 34 | def url_chooser(account,status): 35 | title="Open URL" 36 | prompt="Select a URL?" 37 | type=chooser.ChooseGui.TYPE_URL 38 | if hasattr(status,"message_create"): 39 | urlList=utils.find_urls_in_text(status.message_create['message_data']['text']) 40 | else: 41 | urlList = utils.find_urls_in_text(status.text) 42 | if len(urlList) == 1 and globals.prefs.autoOpenSingleURL: 43 | utils.openURL(urlList[0]) 44 | else: 45 | chooser.chooser(account,title,prompt,urlList,type) 46 | 47 | def follow(account,status): 48 | u=utils.get_user_objects_in_tweet(account,status) 49 | u2=[] 50 | for i in u: 51 | u2.append(i.screen_name) 52 | chooser.chooser(account,"Follow User","Follow who?",u2,"follow") 53 | 54 | def follow_user(account,username): 55 | try: 56 | user=account.follow(username) 57 | sound.play(globals.currentAccount,"follow") 58 | except TweepyException as error: 59 | utils.handle_error(error,"Follow "+username) 60 | 61 | def unfollow(account,status): 62 | u=utils.get_user_objects_in_tweet(account,status) 63 | u2=[] 64 | for i in u: 65 | u2.append(i.screen_name) 66 | chooser.chooser(account,"Unfollow User","Unfollow who?",u2,"unfollow") 67 | 68 | def unfollow_user(account,username): 69 | try: 70 | user=account.unfollow(username) 71 | sound.play(globals.currentAccount,"unfollow") 72 | except TweepyException as error: 73 | utils.handle_error(error,"Unfollow "+username) 74 | 75 | def block(account,status): 76 | u=utils.get_user_objects_in_tweet(account,status) 77 | u2=[] 78 | for i in u: 79 | u2.append(i.screen_name) 80 | chooser.chooser(account,"Block User","Block who?",u2,"block") 81 | 82 | def unblock(account,status): 83 | u=utils.get_user_objects_in_tweet(account,status) 84 | u2=[] 85 | for i in u: 86 | u2.append(i.screen_name) 87 | chooser.chooser(account,"Unblock User","Unblock who?",u2,"block") 88 | 89 | def mute(account,status): 90 | u=utils.get_user_objects_in_tweet(account,status) 91 | u2=[] 92 | for i in u: 93 | u2.append(i.screen_name) 94 | chooser.chooser(account,"Mute User","Mute who?",u2,"mute") 95 | 96 | def unmute(account,status): 97 | u=utils.get_user_objects_in_tweet(account,status) 98 | u2=[] 99 | for i in u: 100 | u2.append(i.screen_name) 101 | chooser.chooser(account,"Unmute User","Unmute who?",u2,"unmute") 102 | 103 | def add_to_list(account,status): 104 | u=utils.get_user_objects_in_tweet(account,status) 105 | u2=[] 106 | for i in u: 107 | u2.append(i.screen_name) 108 | chooser.chooser(account,"Add user to list","Add who?",u2,"list") 109 | 110 | def remove_from_list(account,status): 111 | u=utils.get_user_objects_in_tweet(account,status) 112 | u2=[] 113 | for i in u: 114 | u2.append(i.screen_name) 115 | chooser.chooser(account,"Remove user from list","Remove who?",u2,"listr") 116 | 117 | def message(account,status): 118 | if hasattr(status,"message_create"): 119 | if status.message_create['sender_id']!=account.me.id: 120 | user=utils.lookup_user(status.message_create['sender_id']).screen_name 121 | else: 122 | user=utils.lookup_user(account,status.message_create['target']['recipient_id']).screen_name 123 | else: 124 | user=status.user.screen_name 125 | message_user(account,user) 126 | 127 | def message_user(account,user): 128 | NewTweet=tweet.TweetGui(account,user,"message") 129 | NewTweet.Show() 130 | 131 | def retweet(account,status): 132 | try: 133 | account.retweet(status.id) 134 | globals.prefs.retweets_sent+=1 135 | sound.play(globals.currentAccount,"send_retweet") 136 | except TweepyException as error: 137 | utils.handle_error(error,"retweet") 138 | 139 | def like(account,status): 140 | try: 141 | if status.favorited: 142 | account.unlike(status.id) 143 | status.favorited=False 144 | sound.play(globals.currentAccount,"unlike") 145 | else: 146 | account.like(status.id) 147 | globals.prefs.likes_sent+=1 148 | status.favorited=True 149 | sound.play(globals.currentAccount,"like") 150 | except TweepyException as error: 151 | utils.handle_error(error,"like tweet") 152 | 153 | def followers(account,id=-1): 154 | if id==-1: 155 | id=account.me.id 156 | flw=view.UserViewGui(account,account.followers(id=id),"Followers") 157 | flw.Show() 158 | 159 | def friends(account,id=-1): 160 | if id==-1: 161 | id=account.me.id 162 | flw=view.UserViewGui(account,account.friends(id=id),"Friends") 163 | flw.Show() 164 | 165 | def mutual_following(account): 166 | if account.me.friends_count>globals.prefs.user_limit*200 or account.me.followers_count>globals.prefs.user_limit*200: 167 | if account.me.friends_count>account.me.followers_count: 168 | calls=math.ceil(account.me.friends_count/200) 169 | else: 170 | calls=math.ceil(account.me.followers_count/200,0) 171 | utils.alert("Your set number of user API calls don't allow for this analysis. This means that you have more followers or friends than the API calls would return, thus making this analysis impossible. You would need to perform "+str(calls)+" calls for this analysis to work.","Error") 172 | return 173 | flw=view.UserViewGui(account,account.mutual_following(),"Mutual followers") 174 | flw.Show() 175 | 176 | def not_following_me(account): 177 | if account.me.friends_count>globals.prefs.user_limit*200 or account.me.followers_count>globals.prefs.user_limit*200: 178 | if account.me.friends_count>account.me.followers_count: 179 | calls=math.ceil(account.me.friends_count/200) 180 | else: 181 | calls=math.ceil(account.me.followers_count/200) 182 | utils.alert("Your set number of user API calls doesn't allow for this analysis. This means that you have more followers or friends than the API calls would return, thus making this analysis impossible. You would need to perform "+str(calls)+" calls for this analysis to work.","Error") 183 | return 184 | flw=view.UserViewGui(account,account.not_following_me(),"Users not following me") 185 | flw.Show() 186 | 187 | def not_following(account): 188 | flw=view.UserViewGui(account,account.not_following(),"users I don't follow") 189 | flw.Show() 190 | 191 | def havent_tweeted(account): 192 | flw=view.UserViewGui(account,account.havent_tweeted(),"users who haven't tweeted recently") 193 | flw.Show() 194 | 195 | def user_timeline_user(account,username,focus=True): 196 | if username in account.prefs.user_timelines and focus: 197 | utils.alert("You already have a timeline for this user open.","Error") 198 | return False 199 | if len(account.prefs.user_timelines)>=8: 200 | utils.alert("You cannot have this many user timelines open! Please consider using a list instead.","Error") 201 | return False 202 | user=utils.lookup_user_name(account,username) 203 | if user!=-1: 204 | if not focus: 205 | account.timelines.append(timeline.timeline(account,name=username+"'s Timeline",type="user",data=username,user=user,silent=True)) 206 | else: 207 | account.timelines.append(timeline.timeline(account,name=username+"'s Timeline",type="user",data=username,user=user)) 208 | if username not in account.prefs.user_timelines: 209 | account.prefs.user_timelines.append(username) 210 | main.window.refreshTimelines() 211 | if focus: 212 | account.currentIndex=len(account.timelines)-1 213 | main.window.list.SetSelection(len(account.timelines)-1) 214 | main.window.on_list_change(None) 215 | return True 216 | 217 | def search(account,q,focus=True): 218 | if not focus: 219 | account.timelines.append(timeline.timeline(account,name=q+" Search",type="search",data=q,silent=True)) 220 | else: 221 | account.timelines.append(timeline.timeline(account,name=q+" Search",type="search",data=q)) 222 | if q not in account.prefs.search_timelines: 223 | account.prefs.search_timelines.append(q) 224 | main.window.refreshTimelines() 225 | if focus: 226 | account.currentIndex=len(account.timelines)-1 227 | main.window.list.SetSelection(len(account.timelines)-1) 228 | main.window.on_list_change(None) 229 | 230 | def user_search(account,q): 231 | users=account.api.search_users(q=q,page=1) 232 | u=view.UserViewGui(account,users,"User search for "+q) 233 | u.Show() 234 | 235 | def list_timeline(account,n, q,focus=True): 236 | if q in account.prefs.list_timelines and focus: 237 | utils.alert("You already have a timeline for this list open!","Error") 238 | return 239 | if len(account.prefs.list_timelines)>=8: 240 | utils.alert("You cannot have this many list timelines open!","Error") 241 | return 242 | if not focus: 243 | account.timelines.append(timeline.timeline(account,name=n+" List",type="list",data=q,silent=True)) 244 | else: 245 | account.timelines.append(timeline.timeline(account,name=n+" List",type="list",data=q)) 246 | if q not in account.prefs.list_timelines: 247 | account.prefs.list_timelines.append(q) 248 | main.window.refreshTimelines() 249 | if focus: 250 | account.currentIndex=len(account.timelines)-1 251 | main.window.list.SetSelection(len(account.timelines)-1) 252 | main.window.on_list_change(None) 253 | 254 | def next_in_thread(account): 255 | status=account.currentTimeline.statuses[account.currentTimeline.index] 256 | if hasattr(status,"in_reply_to_status_id") and status.in_reply_to_status_id!=None: 257 | newindex=utils.find_status(account.currentTimeline,status.in_reply_to_status_id) 258 | if newindex>-1: 259 | account.currentTimeline.index=newindex 260 | main.window.list2.SetSelection(newindex) 261 | else: 262 | sound.play(account,"boundary") 263 | 264 | def previous_in_thread(account): 265 | newindex=-1 266 | newindex=utils.find_reply(account.currentTimeline,account.currentTimeline.statuses[account.currentTimeline.index].id) 267 | if newindex>-1: 268 | account.currentTimeline.index=newindex 269 | main.window.list2.SetSelection(newindex) 270 | else: 271 | sound.play(account,"boundary") 272 | 273 | def previous_from_user(account): 274 | newindex=-1 275 | oldindex=account.currentTimeline.index 276 | user=account.currentTimeline.statuses[account.currentTimeline.index].user 277 | newindex2=0 278 | for i in account.currentTimeline.statuses: 279 | if newindex2>=oldindex: 280 | break 281 | if i.user.id==user.id: 282 | newindex=newindex2 283 | newindex2+=1 284 | 285 | if newindex>-1: 286 | account.currentTimeline.index=newindex 287 | main.window.list2.SetSelection(newindex) 288 | else: 289 | sound.play(account,"boundary") 290 | 291 | def next_from_user(account): 292 | newindex=-1 293 | oldindex=account.currentTimeline.index 294 | status=account.currentTimeline.statuses[account.currentTimeline.index] 295 | user=account.currentTimeline.statuses[account.currentTimeline.index].user 296 | newindex2=0 297 | for i in account.currentTimeline.statuses: 298 | if i!=status and i.user.id==user.id and newindex2>=oldindex: 299 | newindex=newindex2 300 | break 301 | newindex2+=1 302 | 303 | if newindex>-1: 304 | account.currentTimeline.index=newindex 305 | main.window.list2.SetSelection(newindex) 306 | else: 307 | sound.play(account,"boundary") 308 | 309 | def delete(account,status): 310 | try: 311 | account.api2.delete_tweet(id=status.id) 312 | account.currentTimeline.statuses.remove(status) 313 | main.window.list2.Delete(account.currentTimeline.index) 314 | sound.play(globals.currentAccount,"delete") 315 | main.window.list2.SetSelection(account.currentTimeline.index) 316 | except TweepyException as error: 317 | utils.handle_error(error,"Delete tweet") 318 | 319 | def load_conversation(account,status): 320 | for i in account.timelines: 321 | if i.type=="conversation": 322 | return False 323 | account.timelines.append(timeline.timeline(account,name="Conversation with "+status.user.screen_name,type="conversation",data=status.user.screen_name,status=status)) 324 | main.window.refreshTimelines() 325 | main.window.list.SetSelection(len(account.timelines)-1) 326 | account.currentIndex=len(account.timelines)-1 327 | main.window.on_list_change(None) 328 | 329 | def play(status): 330 | if sound.player != None and sound.player.is_playing: 331 | speak.speak("Stopped") 332 | sound.stop() 333 | return 334 | if hasattr(status,"message_create"): 335 | urls=utils.find_urls_in_text(status.message_create['message_data']['text']) 336 | else: 337 | urls=utils.find_urls_in_tweet(status) 338 | try: 339 | speak.speak("Retrieving URL...") 340 | audio=sound.get_audio_urls(urls)[0] 341 | a=audio['func'](audio['url']) 342 | sound.play_url(a) 343 | except: 344 | speak.speak("No audio.") 345 | 346 | def play_external(status): 347 | if hasattr(status,"message_create"): 348 | urls=utils.find_urls_in_text(status.message_create['message_data']['text']) 349 | else: 350 | urls=utils.find_urls_in_tweet(status) 351 | if globals.prefs.media_player!="": 352 | if len(urls)>0: 353 | speak.speak("Opening media...") 354 | audio=sound.get_media_urls(urls)[0] 355 | if platform.system()!="Darwin": 356 | subprocess.run([globals.prefs.media_player, audio['url']]) 357 | else: 358 | os.system("open -a "+globals.prefs.media_player+" --args "+audio['url']) 359 | else: 360 | speak.speak("No audio") 361 | else: 362 | speak.speak("No external media player setup.") 363 | -------------------------------------------------------------------------------- /GUI/options.py: -------------------------------------------------------------------------------- 1 | import timeline 2 | import platform 3 | import os, sys 4 | import globals 5 | import wx 6 | from . import main 7 | 8 | class general(wx.Panel, wx.Dialog): 9 | def __init__(self, parent): 10 | super(general, self).__init__(parent) 11 | self.main_box = wx.BoxSizer(wx.VERTICAL) 12 | self.ask_dismiss=wx.CheckBox(self, -1, "Ask before dismissing timelines") 13 | self.main_box.Add(self.ask_dismiss, 0, wx.ALL, 10) 14 | self.ask_dismiss.SetValue(globals.prefs.ask_dismiss) 15 | self.earcon_audio=wx.CheckBox(self, -1, "Play a sound when a tweet contains media") 16 | self.main_box.Add(self.earcon_audio, 0, wx.ALL, 10) 17 | self.earcon_audio.SetValue(globals.prefs.earcon_audio) 18 | self.earcon_top=wx.CheckBox(self, -1, "Play a sound when you navigate to a timeline that may have new items") 19 | self.main_box.Add(self.earcon_top, 0, wx.ALL, 10) 20 | self.earcon_top.SetValue(globals.prefs.earcon_top) 21 | self.demojify=wx.CheckBox(self, -1, "Remove emojis and other unicode characters from display names") 22 | self.main_box.Add(self.demojify, 0, wx.ALL, 10) 23 | self.demojify.SetValue(globals.prefs.demojify) 24 | self.demojify_tweet=wx.CheckBox(self, -1, "Remove emojis and other unicode characters from tweet text") 25 | self.main_box.Add(self.demojify_tweet, 0, wx.ALL, 10) 26 | self.demojify_tweet.SetValue(globals.prefs.demojify_tweet) 27 | self.reversed=wx.CheckBox(self, -1, "Reverse timelines (newest on bottom)") 28 | self.main_box.Add(self.reversed, 0, wx.ALL, 10) 29 | self.reversed.SetValue(globals.prefs.reversed) 30 | self.wrap=wx.CheckBox(self, -1, "Word wrap in text fields") 31 | self.main_box.Add(self.wrap, 0, wx.ALL, 10) 32 | self.wrap.SetValue(globals.prefs.wrap) 33 | self.errors=wx.CheckBox(self, -1, "Play sound and speak message for errors") 34 | self.main_box.Add(self.errors, 0, wx.ALL, 10) 35 | self.errors.SetValue(globals.prefs.errors) 36 | self.autoOpenSingleURL=wx.CheckBox(self, -1, "when getting URLs from a tweet, automatically open the first URL if it is the only one") 37 | self.main_box.Add(self.autoOpenSingleURL, 0, wx.ALL, 10) 38 | self.autoOpenSingleURL.SetValue(globals.prefs.autoOpenSingleURL) 39 | self.use24HourTime=wx.CheckBox(self, -1, "Use 24-hour time for tweet timestamps") 40 | self.main_box.Add(self.use24HourTime, 0, wx.ALL, 10) 41 | self.use24HourTime.SetValue(globals.prefs.use24HourTime) 42 | 43 | 44 | class templates(wx.Panel, wx.Dialog): 45 | def __init__(self, parent): 46 | super(templates, self).__init__(parent) 47 | self.main_box = wx.BoxSizer(wx.VERTICAL) 48 | self.tweetTemplate_label = wx.StaticText(self, -1, "Tweet template") 49 | self.tweetTemplate = wx.TextCtrl(self, -1, "") 50 | self.main_box.Add(self.tweetTemplate, 0, wx.ALL, 10) 51 | self.tweetTemplate.AppendText(globals.prefs.tweetTemplate) 52 | self.quoteTemplate_label = wx.StaticText(self, -1, "Quote template") 53 | self.quoteTemplate = wx.TextCtrl(self, -1, "") 54 | self.main_box.Add(self.quoteTemplate, 0, wx.ALL, 10) 55 | self.quoteTemplate.AppendText(globals.prefs.quoteTemplate) 56 | self.retweetTemplate_label = wx.StaticText(self, -1, "Retweet template") 57 | self.retweetTemplate = wx.TextCtrl(self, -1, "") 58 | self.main_box.Add(self.retweetTemplate, 0, wx.ALL, 10) 59 | self.retweetTemplate.AppendText(globals.prefs.retweetTemplate) 60 | self.copyTemplate_label = wx.StaticText(self, -1, "Copy template") 61 | self.copyTemplate = wx.TextCtrl(self, -1, "") 62 | self.main_box.Add(self.copyTemplate, 0, wx.ALL, 10) 63 | self.copyTemplate.AppendText(globals.prefs.copyTemplate) 64 | self.messageTemplate_label = wx.StaticText(self, -1, "Direct Message template") 65 | self.messageTemplate = wx.TextCtrl(self, -1, "") 66 | self.main_box.Add(self.messageTemplate, 0, wx.ALL, 10) 67 | self.messageTemplate.AppendText(globals.prefs.messageTemplate) 68 | self.userTemplate_label = wx.StaticText(self, -1, "User template") 69 | self.userTemplate = wx.TextCtrl(self, -1, "") 70 | self.main_box.Add(self.userTemplate, 0, wx.ALL, 10) 71 | self.userTemplate.AppendText(globals.prefs.userTemplate) 72 | 73 | class advanced(wx.Panel, wx.Dialog): 74 | def __init__(self, parent): 75 | super(advanced, self).__init__(parent) 76 | self.main_box = wx.BoxSizer(wx.VERTICAL) 77 | if platform.system()!="Darwin": 78 | self.invisible=wx.CheckBox(self, -1, "Enable invisible interface") 79 | self.main_box.Add(self.invisible, 0, wx.ALL, 10) 80 | self.invisible.SetValue(globals.prefs.invisible) 81 | self.invisible_sync=wx.CheckBox(self, -1, "Sync invisible interface with UI (uncheck for reduced lag in invisible interface)") 82 | self.main_box.Add(self.invisible_sync, 0, wx.ALL, 10) 83 | self.invisible_sync.SetValue(globals.prefs.invisible_sync) 84 | self.repeat=wx.CheckBox(self, -1, "Repeat items at edges of invisible interface") 85 | self.main_box.Add(self.repeat, 0, wx.ALL, 10) 86 | self.repeat.SetValue(globals.prefs.repeat) 87 | self.position=wx.CheckBox(self, -1, "Speak position information when navigating between timelines of invisible interface and switching timelines") 88 | self.main_box.Add(self.position, 0, wx.ALL, 10) 89 | self.position.SetValue(globals.prefs.position) 90 | self.update_time_label = wx.StaticText(self, -1, "Update time, in minutes") 91 | self.update_time = wx.TextCtrl(self, -1, "") 92 | self.main_box.Add(self.update_time, 0, wx.ALL, 10) 93 | self.update_time.AppendText(str(globals.prefs.update_time)) 94 | self.user_limit_label = wx.StaticText(self, -1, "Max API calls when fetching users in user viewer") 95 | self.user_limit = wx.TextCtrl(self, -1, "") 96 | self.main_box.Add(self.user_limit, 0, wx.ALL, 10) 97 | self.user_limit.AppendText(str(globals.prefs.user_limit)) 98 | self.count_label = wx.StaticText(self, -1, "Number of tweets to fetch per call (Maximum is 200)") 99 | self.count = wx.TextCtrl(self, -1, "") 100 | self.main_box.Add(self.count, 0, wx.ALL, 10) 101 | self.count.AppendText(str(globals.prefs.count)) 102 | self.streaming=wx.CheckBox(self, -1, "Enable streaming for home, mentions, and list timelines (Requires restart to disable)") 103 | self.main_box.Add(self.streaming, 0, wx.ALL, 10) 104 | self.streaming.SetValue(globals.prefs.streaming) 105 | self.media_player_box=wx.StaticBox(self, -1,"Media Player path") 106 | self.media_player=wx.FilePickerCtrl(self.media_player_box, -1, "", "Path to external Media Player") 107 | self.main_box.Add(self.media_player, 0, wx.ALL, 10) 108 | self.media_player.SetPath(globals.prefs.media_player) 109 | self.main_box.Add(self.media_player_box, 0, wx.ALL, 10) 110 | 111 | class OptionsGui(wx.Dialog): 112 | def __init__(self): 113 | wx.Dialog.__init__(self, None, title="Options", size=(350,200)) 114 | self.Bind(wx.EVT_CLOSE, self.OnClose) 115 | self.panel = wx.Panel(self) 116 | self.main_box = wx.BoxSizer(wx.VERTICAL) 117 | self.notebook = wx.Notebook(self.panel) 118 | self.general=general(self.notebook) 119 | self.notebook.AddPage(self.general, "General") 120 | self.general.SetFocus() 121 | self.templates=templates(self.notebook) 122 | self.notebook.AddPage(self.templates, "Templates") 123 | self.advanced=advanced(self.notebook) 124 | self.notebook.AddPage(self.advanced, "Advanced") 125 | self.main_box.Add(self.notebook, 0, wx.ALL, 10) 126 | self.ok = wx.Button(self.panel, wx.ID_OK, "&OK") 127 | self.ok.SetDefault() 128 | self.ok.Bind(wx.EVT_BUTTON, self.OnOK) 129 | self.main_box.Add(self.ok, 0, wx.ALL, 10) 130 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 131 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 132 | self.main_box.Add(self.close, 0, wx.ALL, 10) 133 | self.panel.Layout() 134 | 135 | def OnOK(self, event): 136 | refresh=False 137 | globals.prefs.use24HourTime = self.general.use24HourTime.GetValue() 138 | globals.prefs.ask_dismiss=self.general.ask_dismiss.GetValue() 139 | if platform.system()!="Darwin": 140 | globals.prefs.invisible=self.advanced.invisible.GetValue() 141 | globals.prefs.invisible_sync=self.advanced.invisible_sync.GetValue() 142 | globals.prefs.repeat=self.advanced.repeat.GetValue() 143 | globals.prefs.invisible_sync=self.advanced.invisible_sync.GetValue() 144 | if globals.prefs.invisible and not main.window.invisible: 145 | main.window.register_keys() 146 | if not globals.prefs.invisible and main.window.invisible: 147 | main.window.unregister_keys() 148 | globals.prefs.streaming=self.advanced.streaming.GetValue() 149 | globals.prefs.position=self.advanced.position.GetValue() 150 | globals.prefs.media_player=self.advanced.media_player.GetPath() 151 | globals.prefs.earcon_audio=self.general.earcon_audio.GetValue() 152 | globals.prefs.earcon_top=self.general.earcon_top.GetValue() 153 | globals.prefs.wrap=self.general.wrap.GetValue() 154 | globals.prefs.update_time=int(self.advanced.update_time.GetValue()) 155 | if globals.prefs.update_time<1: 156 | globals.prefs.update_time=1 157 | globals.prefs.user_limit=int(self.advanced.user_limit.GetValue()) 158 | if globals.prefs.user_limit<1: 159 | globals.prefs.user_limit=1 160 | if globals.prefs.user_limit>15: 161 | globals.prefs.user_limit=15 162 | globals.prefs.count=int(self.advanced.count.GetValue()) 163 | if globals.prefs.count>200: 164 | globals.prefs.count=200 165 | if globals.prefs.reversed!=self.general.reversed.GetValue(): 166 | reverse=True 167 | else: 168 | reverse=False 169 | globals.prefs.reversed=self.general.reversed.GetValue() 170 | if globals.prefs.demojify_tweet!=self.general.demojify_tweet.GetValue() or globals.prefs.demojify!=self.general.demojify.GetValue() or globals.prefs.tweetTemplate!=self.templates.tweetTemplate.GetValue() or globals.prefs.retweetTemplate!=self.templates.retweetTemplate.GetValue or globals.prefs.quoteTemplate!=self.templates.quoteTemplate.GetValue or globals.prefs.messageTemplate!=self.templates.messageTemplate.GetValue(): 171 | refresh=True 172 | globals.prefs.demojify=self.general.demojify.GetValue() 173 | globals.prefs.demojify_tweet=self.general.demojify_tweet.GetValue() 174 | globals.prefs.errors=self.general.errors.GetValue() 175 | globals.prefs.tweetTemplate=self.templates.tweetTemplate.GetValue() 176 | globals.prefs.quoteTemplate=self.templates.quoteTemplate.GetValue() 177 | globals.prefs.retweetTemplate=self.templates.retweetTemplate.GetValue() 178 | globals.prefs.messageTemplate=self.templates.messageTemplate.GetValue() 179 | globals.prefs.copyTemplate=self.templates.copyTemplate.GetValue() 180 | globals.prefs.userTemplate=self.templates.userTemplate.GetValue() 181 | globals.prefs.autoOpenSingleURL=self.general.autoOpenSingleURL.GetValue() 182 | self.Destroy() 183 | if reverse: 184 | timeline.reverse() 185 | if refresh: 186 | main.window.refreshList() 187 | 188 | def OnClose(self, event): 189 | self.Destroy() 190 | -------------------------------------------------------------------------------- /GUI/poll.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | class PollGui(wx.Dialog): 4 | def __init__(self): 5 | wx.Dialog.__init__(self, None, title="Compose poll", size=(350,200)) 6 | self.panel = wx.Panel(self) 7 | self.main_box = wx.BoxSizer(wx.VERTICAL) 8 | self.runfor_label = wx.StaticText(self.panel, -1, "Run Poll &for (Days)") 9 | self.runfor = wx.SpinCtrl(self.panel, -1, min=1,max=7) 10 | self.main_box.Add(self.runfor, 0, wx.ALL, 10) 11 | self.runfor.SetFocus() 12 | self.opt1_label = wx.StaticText(self.panel, -1, "Option &1") 13 | self.opt1 = wx.TextCtrl(self.panel, -1, "",style=wx.TE_DONTWRAP) 14 | self.main_box.Add(self.opt1, 0, wx.ALL, 10) 15 | self.opt2_label = wx.StaticText(self.panel, -1, "Option &2") 16 | self.opt2 = wx.TextCtrl(self.panel, -1, "",style=wx.TE_DONTWRAP) 17 | self.main_box.Add(self.opt2, 0, wx.ALL, 10) 18 | self.opt3_label = wx.StaticText(self.panel, -1, "Option &3") 19 | self.opt3 = wx.TextCtrl(self.panel, -1, "",style=wx.TE_DONTWRAP) 20 | self.main_box.Add(self.opt3, 0, wx.ALL, 10) 21 | self.opt4_label = wx.StaticText(self.panel, -1, "Option &4") 22 | self.opt4 = wx.TextCtrl(self.panel, -1, "",style=wx.TE_DONTWRAP) 23 | self.main_box.Add(self.opt4, 0, wx.ALL, 10) 24 | self.ok = wx.Button(self.panel, wx.ID_OK, "&OK") 25 | self.ok.SetDefault() 26 | self.main_box.Add(self.ok, 0, wx.ALL, 10) 27 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 28 | self.main_box.Add(self.close, 0, wx.ALL, 10) 29 | self.panel.Layout() 30 | -------------------------------------------------------------------------------- /GUI/profile.py: -------------------------------------------------------------------------------- 1 | import globals 2 | import wx 3 | class ProfileGui(wx.Dialog): 4 | 5 | def __init__(self, account): 6 | self.account=account 7 | s=account.api.verify_credentials() 8 | wx.Dialog.__init__(self, None, title="Profile Editor", size=(350,200)) # initialize the wx frame 9 | self.Bind(wx.EVT_CLOSE, self.OnClose) 10 | self.panel = wx.Panel(self) 11 | self.main_box = wx.BoxSizer(wx.VERTICAL) 12 | self.name_label = wx.StaticText(self.panel, -1, "Full Name") 13 | self.name = wx.TextCtrl(self.panel, -1, "") 14 | self.main_box.Add(self.name, 0, wx.ALL, 10) 15 | self.name.SetFocus() 16 | if s.name!=None: 17 | self.name.SetValue(s.name) 18 | self.url_label = wx.StaticText(self.panel, -1, "URL") 19 | self.url = wx.TextCtrl(self.panel, -1, "") 20 | self.main_box.Add(self.url, 0, wx.ALL, 10) 21 | if s.url!=None: 22 | self.url.SetValue(s.url) 23 | self.location_label = wx.StaticText(self.panel, -1, "Location") 24 | self.location = wx.TextCtrl(self.panel, -1, "") 25 | self.main_box.Add(self.location, 0, wx.ALL, 10) 26 | if s.location!=None: 27 | self.location.SetValue(s.location) 28 | self.description_label = wx.StaticText(self.panel, -1, "Description") 29 | self.description = wx.TextCtrl(self.panel, -1, "") 30 | self.main_box.Add(self.description, 0, wx.ALL, 10) 31 | if s.description!=None: 32 | self.description.SetValue(s.description) 33 | self.update = wx.Button(self.panel, wx.ID_DEFAULT, "&Update") 34 | self.update.SetDefault() 35 | self.update.Bind(wx.EVT_BUTTON, self.Update) 36 | self.main_box.Add(self.update, 0, wx.ALL, 10) 37 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 38 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 39 | self.main_box.Add(self.close, 0, wx.ALL, 10) 40 | self.panel.Layout() 41 | def Update(self, event): 42 | self.account.UpdateProfile(self.name.GetValue(),self.url.GetValue(),self.location.GetValue(),self.description.GetValue()) 43 | self.Destroy() 44 | def OnClose(self, event): 45 | self.Destroy() -------------------------------------------------------------------------------- /GUI/search.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import globals 3 | from . import misc 4 | 5 | class SearchGui(wx.Dialog): 6 | def __init__(self,account, type="search"): 7 | self.account=account 8 | self.type=type 9 | wx.Dialog.__init__(self, None, title="Search", size=(350,200)) 10 | self.Bind(wx.EVT_CLOSE, self.OnClose) 11 | self.panel = wx.Panel(self) 12 | self.main_box = wx.BoxSizer(wx.VERTICAL) 13 | self.text_label = wx.StaticText(self.panel, -1, "Search text") 14 | self.text = wx.TextCtrl(self.panel, -1, "",style=wx.TE_PROCESS_ENTER|wx.TE_DONTWRAP) 15 | self.main_box.Add(self.text, 0, wx.ALL, 10) 16 | self.text.SetFocus() 17 | self.text.Bind(wx.EVT_TEXT_ENTER, self.Search) 18 | self.search = wx.Button(self.panel, wx.ID_DEFAULT, "&Search") 19 | self.search.SetDefault() 20 | self.search.Bind(wx.EVT_BUTTON, self.Search) 21 | self.main_box.Add(self.search, 0, wx.ALL, 10) 22 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 23 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 24 | self.main_box.Add(self.close, 0, wx.ALL, 10) 25 | self.panel.Layout() 26 | 27 | def Search(self, event): 28 | if self.type=="search": 29 | misc.search(self.account,self.text.GetValue()) 30 | else: 31 | misc.user_search(self.account,self.text.GetValue()) 32 | self.Destroy() 33 | 34 | def OnClose(self, event): 35 | self.Destroy() 36 | -------------------------------------------------------------------------------- /GUI/timelines.py: -------------------------------------------------------------------------------- 1 | import application 2 | import wx 3 | import globals 4 | from . import main, misc 5 | 6 | class HiddenTimelinesGui(wx.Dialog): 7 | def __init__(self,account): 8 | self.account=account 9 | wx.Dialog.__init__(self, None, title="Hidden timelines", size=(350,200)) 10 | self.Bind(wx.EVT_CLOSE, self.OnClose) 11 | self.panel = wx.Panel(self) 12 | self.main_box = wx.BoxSizer(wx.VERTICAL) 13 | self.list_label=wx.StaticText(self.panel, -1, label="&Timelines") 14 | self.list=wx.ListBox(self.panel, -1) 15 | self.main_box.Add(self.list, 0, wx.ALL, 10) 16 | self.list.SetFocus() 17 | self.list.Bind(wx.EVT_LISTBOX, self.on_list_change) 18 | self.add_items() 19 | self.load = wx.Button(self.panel, wx.ID_DEFAULT, "&Unhide") 20 | self.load.SetDefault() 21 | self.load.Bind(wx.EVT_BUTTON, self.Load) 22 | # self.load.Enable(False) 23 | self.main_box.Add(self.load, 0, wx.ALL, 10) 24 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 25 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 26 | self.main_box.Add(self.close, 0, wx.ALL, 10) 27 | self.panel.Layout() 28 | 29 | def add_items(self): 30 | index=0 31 | for i in self.account.list_timelines(True): 32 | self.list.Insert(i.name,self.list.GetCount()) 33 | self.list.SetSelection(0) 34 | 35 | def on_list_change(self,event): 36 | self.load.Enable(True) 37 | 38 | def Load(self, event): 39 | self.account.list_timelines(True)[self.list.GetSelection()].unhide_tl() 40 | self.list.Delete(self.list.GetSelection()) 41 | 42 | def OnClose(self, event): 43 | self.Destroy() 44 | -------------------------------------------------------------------------------- /GUI/tray.py: -------------------------------------------------------------------------------- 1 | import wx.adv 2 | from wx import Icon 3 | from . import main 4 | TRAY_TOOLTIP = 'Quinter' 5 | TRAY_ICON = 'icon.png' 6 | 7 | def create_menu_item(menu, label, func): 8 | item = wx.MenuItem(menu, -1, label) 9 | menu.Bind(wx.EVT_MENU, func, id=item.GetId()) 10 | menu.Append(item) 11 | return item 12 | 13 | class TaskBarIcon(wx.adv.TaskBarIcon): 14 | def __init__(self, frame): 15 | self.frame = frame 16 | super(TaskBarIcon, self).__init__() 17 | self.set_icon(None) 18 | self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.on_left_down) 19 | 20 | def CreatePopupMenu(self): 21 | menu = wx.Menu() 22 | create_menu_item(menu, 'New tweet', self.frame.OnTweet) 23 | if self.frame.IsShown(): 24 | create_menu_item(menu, 'Hide window', self.OnShowHide) 25 | else: 26 | create_menu_item(menu, 'Show window', self.OnShowHide) 27 | create_menu_item(menu, 'Exit', self.on_exit) 28 | return menu 29 | 30 | def on_left_down(self, event): 31 | self.OnShowHide(event) 32 | 33 | def OnShowHide(self, event): 34 | self.frame.ToggleWindow() 35 | 36 | def on_exit(self, event, blah=True): 37 | self.Destroy() 38 | if blah: 39 | self.frame.OnClose(event) 40 | 41 | def set_icon(self, path): 42 | # icon = wx.Icon(wx.Bitmap(path)) 43 | self.SetIcon(Icon(), TRAY_TOOLTIP) -------------------------------------------------------------------------------- /GUI/tweet.py: -------------------------------------------------------------------------------- 1 | from tweepy import TweepyException 2 | import speak 3 | import wx 4 | import globals 5 | import sound 6 | import utils 7 | import platform 8 | if platform.system()!="Darwin": 9 | import twitter_text.parse_tweet 10 | from . import poll 11 | 12 | text_box_size=(800,600) 13 | class TweetGui(wx.Dialog): 14 | def __init__(self,account,inittext="",type="tweet",status=None): 15 | self.ids=[] 16 | self.account=account 17 | self.inittext=inittext 18 | self.max_length=0 19 | self.status=status 20 | self.type=type 21 | self.poll_runfor=None 22 | self.poll_opt1=None 23 | self.poll_opt2=None 24 | self.poll_opt3=None 25 | self.poll_opt4=None 26 | wx.Dialog.__init__(self, None, title=type, size=(350,200)) 27 | self.Bind(wx.EVT_CLOSE, self.OnClose) 28 | self.panel = wx.Panel(self) 29 | self.main_box = wx.BoxSizer(wx.VERTICAL) 30 | self.text_label = wx.StaticText(self.panel, -1, "Te&xt") 31 | if globals.prefs.wrap: 32 | self.text = wx.TextCtrl(self.panel, -1, "",style=wx.TE_MULTILINE,size=text_box_size) 33 | else: 34 | self.text = wx.TextCtrl(self.panel, -1, "",style=wx.TE_MULTILINE|wx.TE_DONTWRAP,size=text_box_size) 35 | if platform.system()=="Darwin": 36 | self.text.MacCheckSpelling(True) 37 | self.main_box.Add(self.text, 0, wx.ALL, 10) 38 | self.text.SetFocus() 39 | self.text.Bind(wx.EVT_TEXT, self.Chars) 40 | if self.type!="message": 41 | self.text.AppendText(inittext) 42 | cursorpos=len(inittext) 43 | else: 44 | cursorpos=0 45 | if self.type=="message": 46 | self.max_length=10000 47 | else: 48 | self.max_length=280 49 | if self.type=="message": 50 | self.text2_label = wx.StaticText(self.panel, -1, "Recipient") 51 | if self.type=="reply" or self.type=="quote" or self.type=="message": 52 | if self.type=="message": 53 | self.text2 = wx.TextCtrl(self.panel, -1, "",style=wx.TE_DONTWRAP,size=text_box_size) 54 | else: 55 | self.text2 = wx.TextCtrl(self.panel, -1, "",style=wx.TE_MULTILINE|wx.TE_DONTWRAP|wx.TE_READONLY,size=text_box_size) 56 | self.main_box.Add(self.text2, 0, wx.ALL, 10) 57 | if self.type=="message": 58 | self.text2.AppendText(inittext) 59 | else: 60 | self.text2.AppendText(status.user.screen_name+": "+status.text) 61 | if self.account.prefs.footer!="": 62 | self.text.AppendText(" "+self.account.prefs.footer) 63 | self.text.SetInsertionPoint(cursorpos) 64 | if self.type!="message": 65 | self.reply_settings_label=wx.StaticText(self.panel, -1, "Who can reply?") 66 | self.reply_settings=wx.Choice(self.panel,-1,size=(800,600)) 67 | self.reply_settings.Insert("Everyone",self.reply_settings.GetCount()) 68 | self.reply_settings.Insert("Mentioned Users Only",self.reply_settings.GetCount()) 69 | self.reply_settings.Insert("Following Only",self.reply_settings.GetCount()) 70 | self.reply_settings.SetSelection(0) 71 | self.main_box.Add(self.reply_settings, 0, wx.ALL, 10) 72 | if platform.system()=="Darwin": 73 | self.autocomplete = wx.Button(self.panel, wx.ID_DEFAULT, "User A&utocomplete") 74 | else: 75 | self.autocomplete = wx.Button(self.panel, wx.ID_DEFAULT, "User &Autocomplete") 76 | self.autocomplete.Bind(wx.EVT_BUTTON, self.Autocomplete) 77 | self.main_box.Add(self.autocomplete, 0, wx.ALL, 10) 78 | if self.type!="reply" and self.type!="message": 79 | self.poll = wx.Button(self.panel, wx.ID_DEFAULT, "Poll") 80 | self.poll.Bind(wx.EVT_BUTTON, self.Poll) 81 | self.main_box.Add(self.poll, 0, wx.ALL, 10) 82 | if self.type=="reply" and self.status!=None and "user_mentions" in self.status.entities and len(self.status.entities['user_mentions'])>0: 83 | self.users=utils.get_user_objects_in_tweet(self.account,self.status,True,True) 84 | self.list_label=wx.StaticText(self.panel, -1, label="&Users to include in reply") 85 | self.list=wx.CheckListBox(self.panel, -1) 86 | self.main_box.Add(self.list, 0, wx.ALL, 10) 87 | for i in self.users: 88 | self.list.Append(i.name+" ("+i.screen_name+")") 89 | self.list.Check(self.list.GetCount()-1,True) 90 | self.list.SetSelection(0) 91 | self.list.Bind(wx.EVT_CHECKLISTBOX,self.OnToggle) 92 | if self.type=="tweet" or self.type=="reply": 93 | self.thread=wx.CheckBox(self.panel, -1, "&Thread mode") 94 | self.main_box.Add(self.thread, 0, wx.ALL, 10) 95 | self.tweet = wx.Button(self.panel, wx.ID_DEFAULT, "&Send") 96 | # self.tweet.SetDefault() 97 | self.tweet.Bind(wx.EVT_BUTTON, self.Tweet) 98 | self.main_box.Add(self.tweet, 0, wx.ALL, 10) 99 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel") 100 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 101 | self.main_box.Add(self.close, 0, wx.ALL, 10) 102 | self.Chars(None) 103 | self.text.Bind(wx.EVT_CHAR, self.onKeyPress) 104 | self.panel.Layout() 105 | 106 | def Poll(self,event): 107 | p=poll.PollGui() 108 | result=p.ShowModal() 109 | if result==wx.ID_CANCEL: return False 110 | self.poll_runfor=p.runfor.GetValue()*60*24 111 | self.poll_opt1=p.opt1.GetValue() 112 | self.poll_opt2=p.opt2.GetValue() 113 | self.poll_opt3=p.opt3.GetValue() 114 | self.poll_opt4=p.opt4.GetValue() 115 | self.poll.Enable(False) 116 | 117 | def onKeyPress(self,event): 118 | mods = event.HasAnyModifiers() 119 | keycode = event.GetKeyCode() 120 | if keycode == wx.WXK_RETURN: 121 | if not mods: 122 | self.Tweet(None) 123 | event.Skip() 124 | 125 | def OnToggle(self,event): 126 | index=event.GetInt() 127 | if self.list.IsChecked(index): 128 | speak.speak("Checked") 129 | else: 130 | speak.speak("Unchecked.") 131 | 132 | def Autocomplete(self,event): 133 | if self.type=="message": 134 | txt=self.text2.GetValue().split(" ") 135 | else: 136 | txt=self.text.GetValue().split(" ") 137 | text="" 138 | for i in txt: 139 | if (self.type!="message" and i.startswith("@") or self.type=="message") and utils.lookup_user_name(self.account,i.strip("@"),False)==-1: 140 | text=i.strip("@") 141 | 142 | if text=="": 143 | speak.speak("No user to autocomplete") 144 | return 145 | self.menu = wx.Menu() 146 | for i in globals.users: 147 | if i.screen_name.lower().startswith(text.lower()) or i.name.lower().startswith(text.lower()): 148 | self.create_menu_item(self.menu, i.name+" (@"+i.screen_name+")", lambda event, orig=text, text=i.screen_name: self.OnUser(event,orig, text)) 149 | self.PopupMenu(self.menu) 150 | 151 | def Newline(self,event): 152 | if platform.system()=="Darwin": 153 | nl="\n" 154 | else: 155 | nl="\r\n" 156 | self.text.WriteText(nl) 157 | 158 | def create_menu_item(self,menu, label, func): 159 | item = wx.MenuItem(menu, -1, label) 160 | menu.Bind(wx.EVT_MENU, func, id=item.GetId()) 161 | menu.Append(item) 162 | return item 163 | 164 | def OnUser(self,event, orig, text): 165 | if self.type!="message": 166 | v=self.text.GetValue().replace(orig,text) 167 | self.text.SetValue(v) 168 | self.text.SetInsertionPoint(len(v)) 169 | else: 170 | v=self.text2.GetValue().replace(orig,text) 171 | self.text2.SetValue(v) 172 | 173 | def next_thread(self): 174 | self.text.SetValue("") 175 | self.text.AppendText(self.inittext) 176 | cursorpos=len(self.inittext) 177 | if self.account.prefs.footer!="": 178 | self.text.AppendText(" "+self.account.prefs.footer) 179 | self.text.SetInsertionPoint(cursorpos) 180 | 181 | def maximum(self): 182 | sound.play(self.account,"max_length") 183 | 184 | def Chars(self, event): 185 | if platform.system()!="Darwin": 186 | results=twitter_text.parse_tweet(self.text.GetValue()) 187 | length=results.weightedLength 188 | else: 189 | length=len(self.text.GetValue()) 190 | if length>0 and self.max_length>0: 191 | percent=str(int((length/self.max_length)*100)) 192 | else: 193 | percent="0" 194 | if self.max_length>0 and length>self.max_length: 195 | self.maximum() 196 | self.SetLabel(self.type+" - "+str(length).split(".")[0]+" of "+str(self.max_length)+" characters ("+percent+" Percent)") 197 | 198 | def Tweet(self, event): 199 | snd="" 200 | if self.type!="message": 201 | if self.reply_settings.GetSelection()==0: ReplySettings=None 202 | elif self.reply_settings.GetSelection()==1: ReplySettings="mentionedUsers" 203 | elif self.reply_settings.GetSelection()==2: ReplySettings="following" 204 | globals.prefs.tweets_sent+=1 205 | if self.status!=None: 206 | if self.type=="quote": 207 | globals.prefs.quotes_sent+=1 208 | status=self.account.quote(self.status, self.text.GetValue()) 209 | else: 210 | globals.prefs.replies_sent+=1 211 | if self.type=="reply": 212 | index=0 213 | if hasattr(self,"list"): 214 | for i in self.users: 215 | if not self.list.IsChecked(index): 216 | self.ids.append(str(i.id)) 217 | index+=1 218 | status=self.account.tweet(text=self.text.GetValue(),id=self.status.id,exclude_reply_user_ids=self.ids,reply_settings=ReplySettings) 219 | else: 220 | status=self.account.tweet(self.text.GetValue(),self.status.id,reply_settings=ReplySettings) 221 | else: 222 | if self.poll_opt1!=None and self.poll_opt1!="": 223 | opts=[] 224 | if self.poll_opt1!="" and self.poll_opt1!=None: opts.append(self.poll_opt1) 225 | if self.poll_opt2!="" and self.poll_opt2!=None: opts.append(self.poll_opt2) 226 | if self.poll_opt3!="" and self.poll_opt3!=None: opts.append(self.poll_opt3) 227 | if self.poll_opt4!="" and self.poll_opt4!=None: opts.append(self.poll_opt4) 228 | status=self.account.tweet(self.text.GetValue(),id=None, reply_settings=ReplySettings, poll_duration_minutes=self.poll_runfor, poll_options=opts) 229 | else: 230 | status=self.account.tweet(self.text.GetValue(),reply_settings=ReplySettings) 231 | globals.prefs.chars_sent+=len(self.text.GetValue()) 232 | else: 233 | id=None 234 | user=utils.lookup_user_name(self.account, self.text2.GetValue()) 235 | if user!=-1: 236 | id=user.id 237 | try: 238 | status=self.account.api.send_direct_message(recipient_id=id,text=self.text.GetValue()) 239 | except TweepyException as error: 240 | sound.play(self.account,"error") 241 | if hasattr(error,"response") and error.response!=None: 242 | speak.speak(error.response.text) 243 | else: 244 | speak.speak(error.reason) 245 | 246 | if self.type=="reply" or self.type=="quote": 247 | snd="send_reply" 248 | elif self.type=="tweet": 249 | snd="send_tweet" 250 | elif self.type=="message": 251 | snd="send_message" 252 | if status!=False: 253 | sound.play(self.account,snd) 254 | if hasattr(self,"thread") and not self.thread.GetValue() or not hasattr(self, "thread"): 255 | self.Destroy() 256 | else: 257 | self.status=status 258 | self.next_thread() 259 | else: 260 | sound.play(self.account,"error") 261 | def OnClose(self, event): 262 | self.Destroy() 263 | -------------------------------------------------------------------------------- /GUI/view.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from tweepy import TweepyException 3 | import platform 4 | import twishort 5 | import globals 6 | from . import misc 7 | import wx 8 | import utils 9 | import sound 10 | text_box_size=(800,600) 11 | class ViewGui(wx.Dialog): 12 | 13 | def __init__(self,account,status): 14 | self.account=account 15 | if hasattr(status,"message_create"): 16 | self.status=status 17 | self.tweet_text=utils.process_message(self.status,True) 18 | self.type="message" 19 | wx.Dialog.__init__(self, None, title="View Tweet from "+utils.lookup_user(status.message_create['sender_id']).name+" ("+utils.lookup_user(status.message_create['sender_id']).screen_name+")", size=(350,200)) # initialize the wx frame 20 | else: 21 | try: 22 | self.status=account.api.get_status(id=status.id,include_ext_alt_text=True,tweet_mode="extended") 23 | except TweepyException as error: 24 | utils.handle_error(error) 25 | self.Destroy() 26 | return 27 | self.tweet_text=utils.process_tweet(self.status,True) 28 | self.type="tweet" 29 | wx.Dialog.__init__(self, None, title="View Tweet from "+status.user.name+" ("+status.user.screen_name+")", size=(350,200)) # initialize the wx frame 30 | self.Bind(wx.EVT_CLOSE, self.OnClose) 31 | self.panel = wx.Panel(self) 32 | self.main_box = wx.BoxSizer(wx.VERTICAL) 33 | urls=utils.find_urls_in_text(self.tweet_text) 34 | for i in urls: 35 | if "twishort" in i: 36 | self.tweet_text=twishort.get_full_text(twishort.get_twishort_uri(i)) 37 | self.text_label = wx.StaticText(self.panel, -1, "Te&xt") 38 | if globals.prefs.wrap: 39 | self.text = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.TE_MULTILINE, size=text_box_size) 40 | else: 41 | self.text = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.TE_MULTILINE|wx.TE_DONTWRAP, size=text_box_size) 42 | self.main_box.Add(self.text, 0, wx.ALL, 10) 43 | self.text.SetFocus() 44 | if self.type=="tweet": 45 | self.text.SetValue(self.tweet_text) 46 | else: 47 | self.text.SetValue(status.message_create['message_data']['text']) 48 | if self.type=="tweet": 49 | self.text2_label = wx.StaticText(self.panel, -1, "Tweet &Details") 50 | self.text2 = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.TE_MULTILINE|wx.TE_DONTWRAP, size=text_box_size) 51 | self.main_box.Add(self.text2, 0, wx.ALL, 10) 52 | extra="" 53 | if hasattr(self.status,"extended_entities"): 54 | if "media" in self.status.extended_entities: 55 | index=0 56 | for i in self.status.extended_entities['media']: 57 | index+=1 58 | extra+="Media "+str(index)+"\r\nType: "+i['type']+"\r\nRaw URL: "+i['media_url']+"\r\n" 59 | if i['ext_alt_text']!=None: 60 | extra+="Image description: "+i['ext_alt_text']+"\r\n" 61 | self.text2.SetValue(extra+"Posted: "+utils.parse_date(self.status.created_at)+"\r\nFrom: "+self.status.source+"\r\nLiked "+str(self.status.favorite_count)+" times\r\nRetweeted "+str(self.status.retweet_count)+" times.") 62 | if platform.system()=="Darwin": 63 | self.text2.SetValue(self.text2.GetValue().replace("\r","")) 64 | self.view_orig = wx.Button(self.panel, -1, "&Original tweet") 65 | self.view_orig.Bind(wx.EVT_BUTTON, self.OnViewOrig) 66 | self.main_box.Add(self.view_orig, 0, wx.ALL, 10) 67 | self.view_retweeters = wx.Button(self.panel, -1, "&View Retweeters") 68 | self.view_retweeters.Bind(wx.EVT_BUTTON, self.OnViewRetweeters) 69 | self.main_box.Add(self.view_retweeters, 0, wx.ALL, 10) 70 | if self.status.retweet_count==0: 71 | self.view_retweeters.Enable(False) 72 | if not hasattr(self.status,"retweeted_status") and not hasattr(self.status,"quoted_status"): 73 | self.view_orig.Enable(False) 74 | self.view_image = wx.Button(self.panel, -1, "&View Image") 75 | self.view_image.Bind(wx.EVT_BUTTON, self.OnViewImage) 76 | self.main_box.Add(self.view_image, 0, wx.ALL, 10) 77 | if not hasattr(self.status,"extended_entities") or hasattr(self.status,"extended_entities") and self.status.extended_entities['media'] == 0: 78 | self.view_image.Enable(False) 79 | self.reply = wx.Button(self.panel, -1, "&Reply") 80 | self.reply.Bind(wx.EVT_BUTTON, self.OnReply) 81 | self.main_box.Add(self.reply, 0, wx.ALL, 10) 82 | self.retweet = wx.Button(self.panel, -1, "R&etweet") 83 | self.retweet.Bind(wx.EVT_BUTTON, self.OnRetweet) 84 | self.main_box.Add(self.retweet, 0, wx.ALL, 10) 85 | self.like = wx.Button(self.panel, -1, "&Like") 86 | self.like.Bind(wx.EVT_BUTTON, self.OnLike) 87 | self.main_box.Add(self.like, 0, wx.ALL, 10) 88 | if len(utils.get_user_objects_in_tweet(self.account,self.status,True,True))>0: 89 | self.profile = wx.Button(self.panel, -1, "View &Profile of "+self.status.user.name+" and "+str(len(utils.get_user_objects_in_tweet(self.account,self.status,True,True)))+" more") 90 | else: 91 | self.profile = wx.Button(self.panel, -1, "View &Profile of "+self.status.user.name) 92 | 93 | if self.type=="tweet": 94 | self.message = wx.Button(self.panel, -1, "&Message "+self.status.user.name) 95 | else: 96 | self.profile = wx.Button(self.panel, -1, "View &Profile of "+utils.lookup_user(self.status.message_create['sender_id']).name) 97 | self.message = wx.Button(self.panel, -1, "&Message "+utils.lookup_user(self.status.message_create['sender_id']).name) 98 | self.message.Bind(wx.EVT_BUTTON, self.OnMessage) 99 | self.main_box.Add(self.message, 0, wx.ALL, 10) 100 | self.profile.Bind(wx.EVT_BUTTON, self.OnProfile) 101 | self.main_box.Add(self.profile, 0, wx.ALL, 10) 102 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Close") 103 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 104 | self.main_box.Add(self.close, 0, wx.ALL, 10) 105 | self.panel.Layout() 106 | 107 | def OnViewOrig(self,event): 108 | if hasattr(self.status,"retweeted_status"): 109 | v=ViewGui(self.account,self.status.retweeted_status) 110 | v.Show() 111 | elif hasattr(self.status,"quoted_status"): 112 | v=ViewGui(self.account,self.status.quoted_status) 113 | v.Show() 114 | 115 | def OnViewImage(self,event): 116 | v=ViewImageGui(self.status) 117 | v.Show() 118 | 119 | def OnReply(self,event): 120 | misc.reply(self.account,self.status) 121 | 122 | def OnRetweet(self,event): 123 | misc.retweet(self.account,self.status) 124 | 125 | def OnViewRetweeters(self,event): 126 | users=[] 127 | if hasattr(self.status,"retweeted_status"): 128 | r=self.account.api.get_retweets(id=self.status.retweeted_status.id) 129 | else: 130 | r=self.account.api.get_retweets(id=self.status.id) 131 | for i in r: 132 | users.append(i.user) 133 | g=UserViewGui(self.account,users,"Retweeters") 134 | g.Show() 135 | 136 | def OnLike(self,event): 137 | misc.like(self.account,self.status) 138 | 139 | def OnProfile(self,event): 140 | if hasattr(self.status,"message_create"): 141 | u=[utils.lookup_user(self.account,self.status.message_create['sender_id'])] 142 | else: 143 | u=[self.status.user] 144 | u2=utils.get_user_objects_in_tweet(self.account,self.status,True,True) 145 | for i in u2: 146 | u.append(i) 147 | g=UserViewGui(self.account,u) 148 | g.Show() 149 | 150 | def OnMessage(self,event): 151 | misc.message(self.account,self.status) 152 | 153 | def OnClose(self, event): 154 | self.Destroy() 155 | 156 | class UserViewGui(wx.Dialog): 157 | 158 | def __init__(self,account,users=[],title="User Viewer"): 159 | self.account=account 160 | self.index=0 161 | self.users=users 162 | wx.Dialog.__init__(self, None, title=title, size=(350,200)) 163 | self.Bind(wx.EVT_CLOSE, self.OnClose) 164 | self.panel = wx.Panel(self) 165 | self.main_box = wx.BoxSizer(wx.VERTICAL) 166 | self.list_label=wx.StaticText(self.panel, -1, label="&Users") 167 | self.list=wx.ListBox(self.panel, -1) 168 | self.main_box.Add(self.list, 0, wx.ALL, 10) 169 | self.list.Bind(wx.EVT_LISTBOX, self.on_list_change) 170 | for i in self.users: 171 | extra="" 172 | if i.protected: 173 | extra+=", Protected" 174 | if i.following: 175 | extra+=", You follow" 176 | if i.description!="" and i.description!=None: 177 | extra+=", "+i.description 178 | self.list.Insert(i.name+" (@"+i.screen_name+")"+extra,self.list.GetCount()) 179 | self.index=0 180 | self.list.SetSelection(self.index) 181 | if len(self.users)==1: 182 | self.list.Show(False) 183 | else: 184 | self.list.SetFocus() 185 | self.text_label = wx.StaticText(self.panel, -1, "Info") 186 | self.text = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.TE_MULTILINE|wx.TE_DONTWRAP, size=text_box_size) 187 | self.main_box.Add(self.text, 0, wx.ALL, 10) 188 | if len(self.users)==1: 189 | self.text.SetFocus() 190 | self.follow = wx.Button(self.panel, -1, "&Follow") 191 | self.follow.Bind(wx.EVT_BUTTON, self.OnFollow) 192 | self.main_box.Add(self.follow, 0, wx.ALL, 10) 193 | self.unfollow = wx.Button(self.panel, -1, "&Unfollow") 194 | self.unfollow.Bind(wx.EVT_BUTTON, self.OnUnfollow) 195 | self.main_box.Add(self.unfollow, 0, wx.ALL, 10) 196 | self.message = wx.Button(self.panel, -1, "&Message") 197 | self.message.Bind(wx.EVT_BUTTON, self.OnMessage) 198 | self.main_box.Add(self.message, 0, wx.ALL, 10) 199 | self.timeline = wx.Button(self.panel, -1, "&Timeline") 200 | self.timeline.Bind(wx.EVT_BUTTON, self.OnTimeline) 201 | self.main_box.Add(self.timeline, 0, wx.ALL, 10) 202 | self.image = wx.Button(self.panel, -1, "View Profile Ima&ge") 203 | self.image.Bind(wx.EVT_BUTTON, self.OnImage) 204 | self.main_box.Add(self.image, 0, wx.ALL, 10) 205 | self.followers = wx.Button(self.panel, -1, "View Fo&llowers") 206 | self.followers.Bind(wx.EVT_BUTTON, self.OnFollowers) 207 | self.main_box.Add(self.followers, 0, wx.ALL, 10) 208 | self.friends = wx.Button(self.panel, -1, "View F&riends") 209 | self.friends.Bind(wx.EVT_BUTTON, self.OnFriends) 210 | self.main_box.Add(self.friends, 0, wx.ALL, 10) 211 | self.follow.Enable(False) 212 | self.unfollow.Enable(False) 213 | self.timeline.Enable(False) 214 | self.message.Enable(False) 215 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Close") 216 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 217 | self.main_box.Add(self.close, 0, wx.ALL, 10) 218 | self.on_list_change(None) 219 | menu = wx.Menu() 220 | m_speak_user=menu.Append(-1, "Speak user", "speak") 221 | self.Bind(wx.EVT_MENU, self.OnSpeakUser, m_speak_user) 222 | accel=[] 223 | accel.append((wx.ACCEL_CTRL, ord(';'), m_speak_user.GetId())) 224 | accel_tbl=wx.AcceleratorTable(accel) 225 | self.SetAcceleratorTable(accel_tbl) 226 | self.panel.Layout() 227 | 228 | def OnSpeakUser(self,event): 229 | self.index=self.list.GetSelection() 230 | user=self.users[self.index].screen_name 231 | utils.speak_user(globals.currentAccount,[user]) 232 | 233 | def on_list_change(self,event): 234 | self.index=self.list.GetSelection() 235 | user=self.users[self.index] 236 | if user.following: 237 | self.unfollow.Enable(True) 238 | self.follow.Enable(False) 239 | else: 240 | self.unfollow.Enable(False) 241 | self.follow.Enable(True) 242 | self.message.Enable(True) 243 | self.timeline.Enable(True) 244 | 245 | extra="" 246 | if hasattr(user,"entities") and "url" in user.entities and "urls" in user.entities['url']: 247 | for i in user.entities['url']['urls']: 248 | extra+="\r\nURL: "+i['expanded_url'] 249 | if hasattr(user,"status"): 250 | extra+="\r\nLast tweeted: "+utils.parse_date(user.status.created_at) 251 | self.text.SetValue("Name: "+user.name+"\r\nScreen Name: "+user.screen_name+"\r\nLocation: "+user.location+"\r\nBio: "+str(user.description)+extra+"\r\nFollowers: "+str(user.followers_count)+"\r\nFriends: "+str(user.friends_count)+"\r\nTweets: "+str(user.statuses_count)+"\r\nLikes: "+str(user.favourites_count)+"\r\nCreated: "+utils.parse_date(user.created_at)+"\r\nProtected: "+str(user.protected)+"\r\nFollowing: "+str(user.following)+"\r\nNotifications enabled: "+str(user.notifications)) 252 | if platform.system()=="Darwin": 253 | self.text.SetValue(self.text.GetValue().replace("\r","")) 254 | 255 | def OnFollow(self,event): 256 | user=self.users[self.index] 257 | misc.follow_user(self.account,user.screen_name) 258 | 259 | def OnUnfollow(self,event): 260 | user=self.users[self.index] 261 | misc.unfollow_user(self.account,user.screen_name) 262 | 263 | def OnFollowers(self,event): 264 | user=self.users[self.index] 265 | misc.followers(self.account,user.id) 266 | 267 | def OnFriends(self,event): 268 | user=self.users[self.index] 269 | misc.friends(self.account,user.id) 270 | 271 | def OnMessage(self,event): 272 | user=self.users[self.index] 273 | misc.message_user(self.account,user.screen_name) 274 | 275 | def OnTimeline(self,event): 276 | user=self.users[self.index] 277 | misc.user_timeline_user(self.account,user.screen_name) 278 | 279 | def OnImage(self,event): 280 | user=self.users[self.index] 281 | v=ViewImageGui(user) 282 | v.Show() 283 | 284 | def OnClose(self, event): 285 | """App close event handler""" 286 | self.Destroy() 287 | 288 | class ViewTextGui(wx.Dialog): 289 | 290 | def __init__(self,text): 291 | wx.Dialog.__init__(self, None, title="Text", size=(350,200)) # initialize the wx frame 292 | self.Bind(wx.EVT_CLOSE, self.OnClose) 293 | self.panel = wx.Panel(self) 294 | self.main_box = wx.BoxSizer(wx.VERTICAL) 295 | self.text_label = wx.StaticText(self.panel, -1, "Te&xt") 296 | self.text = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.TE_MULTILINE|wx.TE_DONTWRAP) 297 | self.main_box.Add(self.text, 0, wx.ALL, 10) 298 | self.text.SetValue(text) 299 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Close") 300 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 301 | self.main_box.Add(self.close, 0, wx.ALL, 10) 302 | self.panel.Layout() 303 | 304 | def OnClose(self, event): 305 | self.Destroy() 306 | 307 | class ViewImageGui(wx.Dialog): 308 | 309 | def __init__(self,status): 310 | self.url=None 311 | if hasattr(status,"profile_image_url_https"): 312 | self.url=status.profile_image_url_https 313 | elif hasattr(status,"extended_entities"): 314 | if "media" in status.extended_entities: 315 | for i in status.extended_entities['media']: 316 | self.url=i['media_url'] 317 | break 318 | image=requests.get(self.url) 319 | f=open(globals.confpath+"/temp_image","wb") 320 | f.write(image.content) 321 | f.close() 322 | self.image=wx.Image(globals.confpath+"/temp_image",wx.BITMAP_TYPE_ANY).ConvertToBitmap() 323 | self.size=(self.image.GetWidth(), self.image.GetHeight()) 324 | wx.Dialog.__init__(self, None, title="Image", size=self.size) # initialize the wx frame 325 | self.SetClientSize(self.size) 326 | self.Bind(wx.EVT_CLOSE, self.OnClose) 327 | self.panel = wx.Panel(self) 328 | self.text_label = wx.StaticText(self.panel, -1, "Image") 329 | self.text = wx.StaticBitmap(self.panel, -1, self.image, (10, 5), self.size) 330 | self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Close") 331 | self.close.Bind(wx.EVT_BUTTON, self.OnClose) 332 | self.panel.Layout() 333 | 334 | def OnClose(self, event): 335 | self.Destroy() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quinter 2 | 3 | Quinter is a light-weight, robust, accessible Twitter client for Mac and Windows. 4 | 5 | ## Note. 6 | 7 | I have stepped back from Quinter development. It was fun while it lasted, but the code is a mess, and a rewrite isn't work I'm willing to do. That said, I'll still check this repo for open Pull Requests, and merge them. I'm honestly not sure if GitHub let's contributors build releases, but if so, feel free once features get merged. 8 | 9 | ## Running 10 | 11 | ```batch 12 | git clone https://github.com/QuinterApp/Quinter 13 | cd quinter 14 | pip install -r requirements.txt 15 | run.bat 16 | ``` 17 | 18 | ## Building 19 | 20 | ```batch 21 | git clone https://github.com/QuinterApp/Quinter 22 | cd quinter 23 | pip install -r requirements.txt 24 | build.bat 25 | copy.bat 26 | ``` 27 | 28 | ## Contributing. 29 | 30 | We ask that pull requests are submitted always, in order to avoid merge conflicts. In addition, if you have a big feature request/idea, it would be better if an issue is opened first, so we can discuss implementation, details, documentation, etc. 31 | 32 | ## Todo 33 | 34 | * Add a timeline-specific find feature. 35 | * Add bookmarks. 36 | * Add shortcut keys for jumping to timelines 37 | * Relative times 38 | * This won't be possible with our current listview, but maybe in invisible? 39 | * Export buffers feature 40 | * View blocked and muted users. 41 | * add command line arguments for external player 42 | -------------------------------------------------------------------------------- /SAAPI64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/SAAPI64.dll -------------------------------------------------------------------------------- /Tolk.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/Tolk.dll -------------------------------------------------------------------------------- /Tolk.py: -------------------------------------------------------------------------------- 1 | ### 2 | # Product: Tolk 3 | # File: Tolk.py 4 | # Description: Python wrapper module. 5 | # Copyright: (c) 2014, Davy Kager 6 | # License: LGPLv3 7 | ## 8 | 9 | from ctypes import cdll, CFUNCTYPE, c_bool, c_wchar_p 10 | import os 11 | import sys 12 | 13 | if sys.version_info[1] >= 8: 14 | os.add_dll_directory(os.getcwd()) 15 | 16 | _tolk = cdll.Tolk 17 | 18 | _proto_load = CFUNCTYPE(None) 19 | load = _proto_load(("Tolk_Load", _tolk)) 20 | 21 | _proto_is_loaded = CFUNCTYPE(c_bool) 22 | is_loaded = _proto_is_loaded(("Tolk_IsLoaded", _tolk)) 23 | 24 | _proto_unload = CFUNCTYPE(None) 25 | unload = _proto_unload(("Tolk_Unload", _tolk)) 26 | 27 | _proto_try_sapi = CFUNCTYPE(None, c_bool) 28 | _param_try_sapi = (1, "try_sapi"), 29 | try_sapi = _proto_try_sapi(("Tolk_TrySAPI", _tolk), _param_try_sapi) 30 | 31 | _proto_prefer_sapi = CFUNCTYPE(None, c_bool) 32 | _param_prefer_sapi = (1, "prefer_sapi"), 33 | prefer_sapi = _proto_prefer_sapi(("Tolk_PreferSAPI", _tolk), _param_prefer_sapi) 34 | 35 | _proto_detect_screen_reader = CFUNCTYPE(c_wchar_p) 36 | detect_screen_reader = _proto_detect_screen_reader(("Tolk_DetectScreenReader", _tolk)) 37 | 38 | _proto_has_speech = CFUNCTYPE(c_bool) 39 | has_speech = _proto_has_speech(("Tolk_HasSpeech", _tolk)) 40 | 41 | _proto_has_braille = CFUNCTYPE(c_bool) 42 | has_braille = _proto_has_braille(("Tolk_HasBraille", _tolk)) 43 | 44 | _proto_output = CFUNCTYPE(c_bool, c_wchar_p, c_bool) 45 | _param_output = (1, "str"), (1, "interrupt", False) 46 | output = _proto_output(("Tolk_Output", _tolk), _param_output) 47 | 48 | _proto_speak = CFUNCTYPE(c_bool, c_wchar_p, c_bool) 49 | _param_speak = (1, "str"), (1, "interrupt", False) 50 | speak = _proto_speak(("Tolk_Speak", _tolk), _param_speak) 51 | 52 | _proto_braille = CFUNCTYPE(c_bool, c_wchar_p) 53 | _param_braille = (1, "str"), 54 | braille = _proto_braille(("Tolk_Braille", _tolk), _param_braille) 55 | 56 | _proto_is_speaking = CFUNCTYPE(c_bool) 57 | is_speaking = _proto_is_speaking(("Tolk_IsSpeaking", _tolk)) 58 | 59 | _proto_silence = CFUNCTYPE(c_bool) 60 | silence = _proto_silence(("Tolk_Silence", _tolk)) 61 | -------------------------------------------------------------------------------- /application.py: -------------------------------------------------------------------------------- 1 | shortname="quinter" 2 | name = "Quinter" 3 | version = "0.73" 4 | author = "Quin and mason" 5 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | nuitka --windows-company-name=Mellaquin --windows-product-name=Quinter --windows-file-version=0.72 --windows-product-version=0.72 --windows-file-description=Quinter --standalone --python-flag=no_site --include-data-file=keymap.keymap=keymap.keymap --windows-disable-console --output-dir=c:\tempbuild --remove-output quinter.pyw -------------------------------------------------------------------------------- /building and running Quinter on m1.txt: -------------------------------------------------------------------------------- 1 | Step 1. Install homebrew and add it to your path. 2 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 3 | export PATH="/opt/homebrew/bin:$PATH" >> ~/.zshrc 4 | step 2. install python 3.9 and wxpython. 5 | brew install python3 wxpython 6 | step 3. install Quinter's requirements. 7 | pip3.9 install -r requirements.txt 8 | step 4. go into the directory /opt/homebrew/frameworks/python.framework/versions/3.9/lib/python3.9/site-packages/ 9 | step 5. In accessible_output2, delete the lib folder. 10 | step 6. in sound_lib/lib, delete the x86 folder and replace the x64 folder with the one in m1files. 11 | step7. try to run it with python3.9 quinter.pyw 12 | If it runs. 13 | Step 8. Go clone Pyinstaller on github 14 | git clone http://github.com/pyinstaller/pyinstaller 15 | step 9. Build the bootloader. 16 | cd bootloader 17 | python ./waf all 18 | step 10. Install pyinstaller. 19 | cd .. 20 | python3.9 setup.py install 21 | step 11 (to build). run ./compileM1.command from the Quinter source dir. And hopefully the magic will happen. -------------------------------------------------------------------------------- /compile.command: -------------------------------------------------------------------------------- 1 | cd "/users/Mason/Dropbox/projects/python/py3/Quinter" 2 | rm -R macdist 3 | pyinstaller --noupx --clean --windowed --osx-bundle-identifier me.masonasons.quinter quinter.pyw --noconfirm --distpath macdist --workpath macbuild 4 | cp keymac.keymap dist/quinter.app/contents/resources/keymac.keymap 5 | cp -R ../macfiles/ macdist/quinter.app/contents/resources 6 | cp -R sounds macdist/quinter.app 7 | cp -R docs/ macdist/ 8 | rm -R macbuild 9 | rm -R macdist/quinter 10 | rm -R /applications/Quinter.app 11 | cp -R macdist/quinter.app /applications/ 12 | zip -r -X macdist/QuinterMac.zip macdist 13 | rm -R macdist/quinter.app -------------------------------------------------------------------------------- /compileM1.command: -------------------------------------------------------------------------------- 1 | cd "/users/Mason/Dropbox/avoid random evil guy/quinter" 2 | rm -R macdist 3 | pyinstaller --noupx --clean --windowed --osx-bundle-identifier me.masonasons.quinter quinter.pyw --noconfirm --distpath m1dist --workpath m1build 4 | cp keymac.keymap m1dist/quinter.app/contents/resources/keymac.keymap 5 | cp -R ../m1files/ m1dist/quinter.app/contents/resources 6 | cp -R sounds m1dist/quinter.app 7 | cp -R docs/ m1dist/ 8 | rm -R m1build 9 | rm -R m1dist/quinter 10 | rm -R /applications/Quinter.app 11 | cp -R m1dist/quinter.app /applications/ 12 | zip -r -X m1dist/QuinterM1.zip m1dist 13 | rm -R m1dist/quinter.app -------------------------------------------------------------------------------- /copy.bat: -------------------------------------------------------------------------------- 1 | rmdir /S /Q windist 2 | mkdir windist 3 | xcopy /Q /E c:\tempbuild\quinter.dist windist /Y 4 | xcopy /Q /E docs windist /Y 5 | xcopy /Q /E quinter_updater\*.exe windist /Y 6 | xcopy /Q *.dll windist /Y 7 | xcopy /Q /E ..\quinterfiles windist /S /Y 8 | mkdir windist\sounds 9 | xcopy /Q /E sounds windist\sounds /Y 10 | rmdir /S /Q c:\tempbuild 11 | del windist\t*86t.dll 12 | pause -------------------------------------------------------------------------------- /docs/Readme.url: -------------------------------------------------------------------------------- 1 | [{000214A0-0000-0000-C000-000000000046}] 2 | Prop3=19,2 3 | [InternetShortcut] 4 | IDList= 5 | URL=http://quinterapp.github.io/readme.html 6 | -------------------------------------------------------------------------------- /docs/changelog.txt: -------------------------------------------------------------------------------- 1 | 0.73 2 | If you click no on the update available dialog, you no longer get told that a zip couldn't be found for your platform. 3 | Fix list members and subscribers in the lists manager. 4 | 5 | 0.72 6 | Fixes Quinter for mac. You will need to reauth. Twitter broke it, not us. 7 | 8 | 0.71 9 | Added shortcut keys to the controls in the pole dialog. 10 | Now speak a message when exiting. 11 | Fix lookup users, which makes DM's now show recipients properly as well as populates the reply dialog. 12 | The updater code on Mac should now be able to download the zip file properly to your downloads. 13 | The users list in the reply dialog now works properly. 14 | 15 | 0.70 16 | Adds posting of polls. 17 | Fixes reply settings. 18 | 19 | 0.69 20 | Fixes searches. 21 | 22 | 0.68 23 | Updated Tweepy to V4, opening up the possibilities for many new features! 24 | Using API V2 methods for posting and retweeting/quoting/liking tweets. 25 | Add support for setting reply settings for a tweet. This doesn't seem to work? 26 | 27 | 0.67 28 | Auto updater for windows and update downloader for Mac. 29 | Removed a few unneeded DLL files, making the directory cleaner. 30 | 31 | 0.66 32 | Don't load multiple conversations at once. 33 | Checkbox in general tab for speaking errors. 34 | 35 | 0.65 36 | Actually fixed the options dialog, maybe sort of I don't know. 37 | 38 | 0.64 39 | You can now exit with the system tray propperly (thanks Brandon Tyson for reporting)! 40 | Streaming is reenabled, with much better error handling. Note: It may or may not work, depending on your luck, or something. 41 | 42 | 0.63 43 | Streaming has been disabled in this version, as Twitter has nuked the endpoint we were using. 44 | Unblocking is fixed now. 45 | You can now (optionally) view times in 24-hour format (thanks Alex Hall for implementing)! 46 | Minor bugfixes and changes to speech prompts. 47 | Added an option so you can configure how much control+windows+page up/down moves you. 48 | 49 | 0.62 50 | Adds a new user analysis, users I follow that haven't tweeted in a year. Thanks Simon for the suggestion! 51 | Fixes user timelines! 52 | You now hear the boundary sound if at the edge of a thread or specific user's tweets using the hotkeys. 53 | You can no longer scroll through empty timelines like they have items in them. 54 | 55 | 0.61 56 | Adds a built-in facility for updating and downloading QPlay directly from within Quinter! 57 | 58 | 0.60 59 | Adds an option to automatically open URL's if there is only one (Thanks Alex Hall for implementing!) 60 | Adds alt S to open the user menu (Thanks Alex Hall for suggesting!) 61 | The readme has been completely rewritten. It's still missing some information, but you'll have to let us know what you think. 62 | Separates out audio playback into external media player called QPlay. This means quite a few things, quicker load times, less RAM usage, etc. Note: You may have to set your external media player (now called media player) to QPlay manually if you want to use it. Playback functionality directly in the client has been removed. 63 | 64 | 0.59 65 | Fixes list manager unable to properly load with no lists. 66 | If your account API call number could produce false positives, you're now alerted. 67 | Fixes a bug where if you launched the program with too many user timelines open, you would not be able to open any more or close the existing ones from your config. 68 | Adds invisible shortcuts for jumping up and down the timeline by 20 items. 69 | Fixes a bug where date parsing wouldn't properly show the day on yesterday's tweets when the day changes (Thanks Tyler for reporting) 70 | Adds the ability to hide timelines and manage hidden timelines from the timeline menu. 71 | 72 | 0.58 73 | The number of characters is now calculated correctly when multibyte characters are included when composing a tweet. 74 | Fixed the result of "$created_at$" of tweet being strange in some environments. 75 | You can now use control/command enter to insert a newline in new tweet fields. 76 | 77 | 0.57 78 | Emergency fix for sounds not working on some clients. 79 | 80 | 0.56 81 | We now show the bio in user lists. 82 | You can now press control semicolon or command semicolon on mac for speak user summary in user viewer/profile dialogs! 83 | We can now stream user timelines. This is very dependant on a few factors, so it may not stream all the time if at all, but it does when it can. If you load a user timeline for a user you don't follow and wish for it to stream, you will need to restart the program. 84 | Fixed some bugs on direct message receive with remove emojis enabled which in worst cases could cause timeline refreshing to halt. 85 | Fixes (hopefully) some links not appearing correctly in streamed tweets. 86 | Some modifications to the user cache to hopefully majorly reduce lag with switching between buffers. 87 | Replaces clean user database option with refresh user database, which cleans the entire database rather than checking for duplicate users. It is recommended to restart after performing this clean, and not to perform it unless you notice lag. 88 | 89 | 0.55 90 | Fixes quoted tweets with the streaming API... Properly this time. 91 | On Mac OS, native spell checking support is now enabled for the tweet text field. Press command semi or command shift semi for a dialog. Thanks Sarah A for letting us know that this didn't work before! 92 | You can now load Quinter soundpacks either from the user config dir as before, or the sounds directory in the program folder. 93 | We now return upto the first 800 of your or anyone else's followers or friends. 94 | In the menu bar, there is a new users menu, which allows you to perform user analysis such as mutual follows, users you follow that don't follow back, etc. 95 | You can now set how many API calls you'd like user fetches to get. 96 | User account folders no longer spawn in separate folders outside your Quinter folder. Your currrent user accounts will be moved on first launch of this version. 97 | You can now mute and unmute users from the menu. 98 | Muted users should now not show in the streaming API. 99 | 100 | 0.54 101 | Extended quoted tweets and retweeted tweets support to be more compatible with the streaming API. 102 | Fixes mac and streaming support. 103 | 104 | 0.53 105 | Adds an option to remove emojis and other unicode text from tweet text. 106 | SAPI is fixed. 107 | Starts work on streaming support for Quinter. This is not very obvious to find because it's very unstable, and retweets and quote tweets don't always parse correctly, but it's there if you choose to use it. This only works so far on home, mentions, sent, and *some* list timelines. 108 | 109 | 0.52 110 | Switches to another speech output method on Windows. This should fix JAWS. 111 | We now have quoted and retweeted templates. 112 | We now show if a user is protected and their URL in the user viewer. 113 | You can now remove emojis and other unicode characters from user display names. This is experimental. 114 | 115 | 0.51 116 | Puts a limit on how many user timelines and list timelines can be open at a time (8 per each). This was the cause of the problem with the sent timeline failing to update, the user timeline endpoint was being spammed majorly by our client due to way too many user timelines being opened at a time, so we needed to put a limit on this. Sorry for the inconvenience this may cause, and please consider using a list if you would like to see tweets from a large amount of users at once. 117 | We now ignore the API 429 error when pulling timelines. We still send it to the view API errors option in the help menu, so you can still see if it happens, we just don't speak and play a sound if it does. Other errors like rate limit exceeded will still play. 118 | You can no longer open multiple copies of a user or list timeline. 119 | 120 | 0.5 121 | You can now customize how frequently tweets get pulled (do note that if you set this to too small of a value, you'll most likely get API errors). 122 | You can now choose how many tweets get fetched when loading a timeline, or fetching newest tweets. 123 | Increases reliability of timeline updating thread. 124 | Error messages now tell you which action they come from. 125 | If you have multiple accounts, auto read will now tell you which account a tweet comes in from. 126 | You can now choose if Quinter repeats the items at the edges in the invisible interface. 127 | Mute changed from command M on mac to command shift M to prevent minimize conflict. 128 | In the main window, you can now press alt left and right arrow (Option on mac) to switch between timelines. 129 | Fixes System Access (Thanks Jeffrey Stark for reporting! Let us know if it works!) 130 | Adds an option in advanced to disable speaking of position information in invisible interface. 131 | Adds an option to make an earcon play whenever you are not at the top (or bottom if reversed) of a timeline, which may mean you have new items. 132 | 133 | 0.49 134 | Fixes new tweet and a couple others in menu to say the correct shortcut. 135 | Reassigns autoread and mute to alt+win+shift+e and M in invisible interface. 136 | Adds user search (Not working yet)! 137 | Fixes invisible interface not playing the earcons for audio and video tweets. 138 | You no longer hear the ready sound until the account is actually ready. 139 | We no longer have to retrieve a URL in order to stop the currently playing one. 140 | 141 | 0.48 142 | You can now mute/unmute the sounds on a timeline-specific basis. 143 | You can now have specific timelines automatically read tweets as they come in. 144 | The shortcuts are now stated on the menu items. For example, Accounts (control+A). 145 | We no longer tell the keys for the UI in the readme, as they can be found in the menu options themselves. 146 | We now use methods suggested by Tyler Spivey to hopefully make accessing the UI more responsive. I don't notice a difference personally, but someone might? 147 | 148 | 0.47 149 | Changes name of remove button in remove user from list dialog. It was labeled "Add" before. 150 | Adds autocomplete user to new tweet dialog! 151 | Fixes a bad bug where if you have syncing of the interfaces off, you couldn't destroy more than one conversation/timeline. 152 | 153 | 0.46 154 | There is now an option in the advanced options on Windows to force the invisible interface not to sync with the UI, thus reducing lag by a lot. 155 | Fixes a bug with custom soundpacks causing some sounds not to play if they don't exist. 156 | We no longer pull reply usernames from the tweet text, and instead do what you're supposed to do and use the user_mentions entity array. This fixes many problems the mention system had. 157 | You can now press control shift O to open twitter in your web browser directly to the focused tweet. 158 | Thread mode now has an access Key (Thanks NS for notifying about it) 159 | The new tweet/reply/quote/message dialog has been redesigned from the ground up to be more twitter like. Replies no longer take up characters in your tweets, you can easily remove users from threads, and more! 160 | The recipient edit field in the direct message dialog is no longer a multi line edit field. 161 | Sending direct messages should no longer fail as often. 162 | 163 | 0.45 164 | Emergency patch: Fixed a really bad bug resulting from user timeline sounds causing errors if you had ran Quinter after 0.4. 165 | 166 | 0.44 167 | Added a sent tweets buffer. 168 | Resized the text fields a bit, hopefully fixing visuals, and making word wrap a lot better. 169 | If you type an @ symbol before someone's username, it still brings up the profile. 170 | Adds a way in the view tweet and user profile dialogs to view someone's Twitter profile image or the first image in a tweet. Someone will have to let me know how it looks! 171 | 172 | 0.43 173 | Added a new option to the account settings dialog. You can now set the pan of account-specific sounds! 174 | The GUI should (hopefully) look a lot better now, but we're seaking feedback. 175 | 176 | 0.42 177 | You can now press enter in the update profile dialog to save your changes without needing to tab to the button. 178 | There's a new option, allowing you to choose if the text in text fields should wrap onto new lines or not. It's off by default. 179 | Fixed a bug that allowed timelines to break in rare instances. 180 | Added a new global key (control+windows+alt+a) to speak the current account. 181 | You are now alerted if trying to view a tweet that has been deleted (thanks Wren)! 182 | Swapped volume and prev/next from user/thread. Now, volume is alt and prev/next from thread/user is control. 183 | We no longer speak 1 of 0 on empty timelines in the invisible interface. 184 | Quoted tweets should always expand now. 185 | 186 | 0.41 187 | The window title now shows which account you are in. 188 | Regular tweets will send out as proper tweets now. They were doing this before, but Quinter wasn't realizing what it was sending were actually tweets, and it was trying to attach a blank tweet ID to the tweet when it didn't need to. This wasn't hurting anything, it was just something I noticed just now so I fixed it. 189 | Adds the windows alt space key in the invisible interface keymap to speak the currently focused item. 190 | Generalizes some control names. 191 | Adds an account options dialog, where you can set independant account soundpacks and tweet footers. 192 | Fixes reverse timelines. 193 | 194 | 0.4 195 | Fixed previous and next in thread in reversed timelines. 196 | Fixed retweeting in the view tweet dialog. 197 | We now support multiple accounts! See the application menu. Yet another small change log entry for such a massive pain, as well as massive feature, but eh. 198 | Remapped the list hotkeys in the GUI to account for the account manager hotkeys. 199 | 200 | 0.36 201 | Adds a prompt asking if you want to follow @QuinterApp. You only get prompted for this one time upon login. 202 | Fixes a bug where if a timeline didn't have items in it, all timeline updating would stop until you relaunched Quinter. 203 | We now handle a lot more errors, so things won't just silently fail nearly as much anymore. 204 | If a removable timeline (list, search, user) is unable to be loaded or refreshed for any reason, it will be dismissed automatically. 205 | You can now view recent API errors by opening View API errors in the help menu. 206 | You can now reverse timelines. Such a small changelog entry for such a huge pain in the butt, but that's how it goes sometimes, rofl! 207 | 208 | 0.35 209 | When you hit yes to download an update, the program now exits after opening the browser. 210 | If you do not enter a pin, the program will now exit rather than trying to authorize anyway, causing big problems. 211 | We now have sounds for when a tweet contains audio playable by the client as well as audio playable by the external media player only. 212 | If you are unable to message a user, you are now told this instead of the dialog just continuing to sit there. 213 | 214 | 0.33 215 | If a sound doesn't exist in the current soundpack, Quinter no longer throws an error when trying to play it. Instead, it will just play the sound from the default soundpack. 216 | Removed raw symbols such as & you now see the propper symbol. 217 | Adds the advanced dialog to the mac options dialog. 218 | Fixed a bug on mac where if you try to open Quinter for the first time, it does not open but will open the second time. 219 | Adds control left and right brackets (Command on mac) to open followers and friends. 220 | Adds control shift L (Command on mac) to view your lists. 221 | 222 | 0.32 223 | We now allow opening more types of media in external media players. 224 | We now expand Twishort content when you view a tweet. 225 | Setting a list from private to public and from public to private works propperly now. 226 | Fixed searches not destroying from your config when you closed them. 227 | Sounds are now loaded from the config dir. 228 | This is the first very early alpha version for Mac! Note: On mac, please use tab and shift tab and arrow keys. Using the VoiceOver keys will present user interface elements in the wrong order. 229 | 230 | 0.31 231 | Search timelines now persist (Thanks Brian Hartgen for the suggestion!) 232 | Adds a global hotkey for search and view lists. 233 | Sounds for open and close of timeline, as well as conversation update. 234 | More robust dismiss timeline code, which fixes a bug where you could dismiss built-in timelines. 235 | Adds an option to ask before dismissing timelines to the general tab, which is on by default (The previous behavior). 236 | Finishes up list support with many new controls in the view lists dialog. 237 | Fixes update command. 238 | 239 | 0.3 240 | Adds lists support! 241 | Adds twitter searches support. 242 | Adds a label to the external media player box in options. 243 | Adds a check for updates option into a help menu in the interface. 244 | Fixes retweet_count and favorite_count in templates. 245 | You can now copy the current tweet to the clipboard. 246 | Adds user template for speak user and copy template for copying to clipboard. 247 | Adds a tab control to organize the options dialog. 248 | 249 | 0.24 250 | Fixed 0 division error that flooded logs. 251 | Adds keymap support! For now, edit keymap.keymap. 252 | Adds new keys for moving to previous and next in thread, as well as previous and next from same user in invisible interface. 253 | 254 | 0.23 255 | Adds playing of URL's in an external media player. Note: You must set this up in the options dialog. 256 | We now have a basic update checker. If a new version is found, it'll take you right to your web browser and download it. 257 | Add friends and follower keystrokes to the invisible interface. 258 | 259 | 0.22 260 | Adds ability to playback twitter videos, youtube and other audio types. This causes extra ram usage, so let us know if this becomes a problem and is bloated. 261 | We now account for the stupid gen_py bug of accessible_output2. 262 | Speech interrupt for invisible interface. 263 | 264 | 0.21 265 | Messages are now cached. 266 | Window state now saves. 267 | You can now initiate a new tweet from the system tray icon. 268 | 269 | 0.2 270 | Added more information to media in the view tweet dialog. 271 | Added an invisible interface! This is disabled by default, but can be easily enabled. See the readme for a full key list. 272 | 273 | 0.16 274 | Adds view friends and followers buttons to a user profile dialog 275 | Adds conversation view! 276 | 277 | 0.15 278 | Moved all documentation to readme.txt 279 | In the view tweet dialog, you now have the option to view the original source tweet and view any retweeters of a tweet. 280 | Fixed a bug where we would sometimes perform API calls to lookup nonexistent users. 281 | Reclassifies all parent dialogs. No more billions of windows open at once, and you can now use escape. Thanks Josh! 282 | 283 | 0.14 284 | Pressing enter to view a tweet will now grab the tweet in realtime. 285 | You can now see image descriptions/alt text in the view tweet dialog in the second edit field. 286 | 287 | 0.13 288 | Fixed case where user profile grabbage could fail. 289 | User timelines now save on restart. 290 | Fixed character limmits when posting tweets. 291 | Added a few more access keys in some dialogs. 292 | Adds thread mode! Check the thread check box in the tweet or reply dialog, and then you can continue to post tweets on a thread. 293 | Adds ability to delete your own tweets with the delete key. 294 | 295 | 0.12 296 | Whenever text is spoken to the screen reader, it is now Brailled. 297 | The home timeline is now the default. 298 | Added a new key (control + semi colon), to speak a breif overview of the user's profile. 299 | Fixed a bug with user timelines where we were using one more API call than we needed to. 300 | Updated the default direct message template. 301 | If you reply to a tweet with a dot before someone gets mentioned, it's no longer included. 302 | You can now press Control + Shift + Semi Colon to view what a tweet is in reply to. 303 | Alt left and right arrows cycle you between tweets from the same user. 304 | Alt up and down arrows allow you to go through the tweets of a thread. 305 | 306 | 0.11 307 | Fixes a bug that would cause timeline selections to jump around randomly. 308 | Users now update in the user cache database. 309 | Fixes user timelines -------------------------------------------------------------------------------- /globals.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import shutil 3 | import platform 4 | from GUI import main, misc 5 | import tweak 6 | import os 7 | import twitter as t 8 | import pickle 9 | import timeline 10 | import utils 11 | import threading 12 | import sound 13 | accounts=[] 14 | prefs=None 15 | users=[] 16 | unknown_users=[] 17 | confpath="" 18 | errors=[] 19 | currentAccount=None 20 | timeline_settings=[] 21 | def load(): 22 | global timeline_settings 23 | threading.Thread(target=utils.cfu).start() 24 | global confpath 25 | global prefs 26 | global users 27 | prefs=tweak.Config(name="Quinter",autosave=True) 28 | confpath=prefs.user_config_dir 29 | if platform.system()=="Darwin": 30 | try: 31 | f=open(confpath+"/errors.log","a") 32 | sys.stderr=f 33 | except: 34 | pass 35 | if os.path.exists(confpath+"/sounds/default"): 36 | shutil.rmtree(confpath+"/sounds/default") 37 | if not os.path.exists(confpath+"/sounds"): 38 | os.makedirs(confpath+"/sounds") 39 | if platform.system()=="Darwin": 40 | shutil.copytree("/applications/quinter.app/sounds/default",confpath+"/sounds/default") 41 | else: 42 | shutil.copytree("sounds/default",confpath+"/sounds/default") 43 | prefs.timelinecache_version=prefs.get("timelinecache_version",1) 44 | if prefs.timelinecache_version==1: 45 | if os.path.exists(confpath+"/timelinecache"): 46 | os.remove(confpath+"/timelinecache") 47 | prefs.timelinecache_version=2 48 | prefs.user_reversed=prefs.get("user_reversed",False) 49 | prefs.user_limit=prefs.get("user_limit",4) 50 | prefs.tweetTemplate=prefs.get("tweetTemplate","$user.screen_name$: $text$ $created_at$") 51 | prefs.messageTemplate=prefs.get("messageTemplate","$sender.screen_name$ to $recipient.screen_name$: $text$ $created_at$") 52 | prefs.copyTemplate=prefs.get("copyTemplate","$user.name$ ($user.screen_name$): $text$") 53 | prefs.retweetTemplate=prefs.get("retweetTemplate","Retweeting $user.name$ ($user.screen_name$): $text$") 54 | prefs.quoteTemplate=prefs.get("quoteTemplate","Quoting $user.name$ ($user.screen_name$): $text$") 55 | prefs.userTemplate=prefs.get("userTemplate","$name$ ($screen_name$): $followers_count$ followers, $friends_count$ friends, $statuses_count$ tweets. Bio: $description$") 56 | prefs.accounts=prefs.get("accounts",1) 57 | prefs.errors=prefs.get("errors",True) 58 | prefs.streaming=prefs.get("streaming",False) 59 | prefs.invisible=prefs.get("invisible",False) 60 | prefs.invisible_sync=prefs.get("invisible_sync",True) 61 | prefs.update_time=prefs.get("update_time",2) 62 | prefs.volume=prefs.get("volume",1.0) 63 | prefs.count=prefs.get("count",200) 64 | prefs.repeat=prefs.get("repeat",False) 65 | prefs.demojify=prefs.get("demojify",False) 66 | prefs.demojify_tweet=prefs.get("demojify_tweet",False) 67 | prefs.position=prefs.get("position",True) 68 | prefs.chars_sent=prefs.get("chars_sent",0) 69 | prefs.tweets_sent=prefs.get("tweets_sent",0) 70 | prefs.replies_sent=prefs.get("replies_sent",0) 71 | prefs.quotes_sent=prefs.get("quotes_sent",0) 72 | prefs.retweets_sent=prefs.get("retweets_sent",0) 73 | prefs.likes_sent=prefs.get("likes_sent",0) 74 | prefs.statuses_received=prefs.get("statuses_received",0) 75 | prefs.ask_dismiss=prefs.get("ask_dismiss",True) 76 | prefs.reversed=prefs.get("reversed",False) 77 | prefs.window_shown=prefs.get("window_shown",True) 78 | prefs.autoOpenSingleURL=prefs.get("autoOpenSingleURL", False) 79 | prefs.use24HourTime=prefs.get("use24HourTime", False) 80 | if platform.system()!="Darwin": 81 | prefs.media_player=prefs.get("media_player","QPlay.exe") 82 | else: 83 | prefs.media_player=prefs.get("media_player","/applications/QPlay.app") 84 | prefs.earcon_audio=prefs.get("earcon_audio",True) 85 | prefs.earcon_top=prefs.get("earcon_top",False) 86 | prefs.wrap=prefs.get("wrap",False) 87 | if prefs.invisible: 88 | main.window.register_keys() 89 | try: 90 | f=open(confpath+"/usercache","rb") 91 | users=pickle.loads(f.read()) 92 | f.close() 93 | except: 94 | pass 95 | if not prefs.user_reversed: 96 | users=[] 97 | prefs.user_reversed=True 98 | load_timeline_settings() 99 | for i in range(0,prefs.accounts): 100 | add_session() 101 | if platform.system()=="Windows" and not os.path.exists("QPlay.exe"): 102 | q=utils.question("QPlay","It appears you do not have QPlay. It is not needed unless you plan to play audio (such as twitter videos and youtube URL's) without using your browser. Would you like me to set up QPlay for you? Once you hit yes, you can use Quinter normally until QPlay is ready to go.") 103 | if q==1: 104 | threading.Thread(target=utils.download_QPlay).start() 105 | 106 | def add_session(): 107 | global accounts 108 | accounts.append(t.twitter(len(accounts))) 109 | 110 | def save_users(): 111 | global users 112 | f=open(confpath+"/usercache","wb") 113 | f.write(pickle.dumps(users)) 114 | f.close() 115 | 116 | def save_messages(account,messages): 117 | f=open(account.confpath+"/messagecache","wb") 118 | f.write(pickle.dumps(messages)) 119 | f.close() 120 | 121 | def load_messages(account): 122 | try: 123 | f=open(account.confpath+"/messagecache","rb") 124 | messages=pickle.loads(f.read()) 125 | f.close() 126 | return messages 127 | except: 128 | return None 129 | 130 | def save_timeline_settings(): 131 | global confpath 132 | global timeline_settings 133 | f=open(confpath+"/timelinecache","wb") 134 | f.write(pickle.dumps(timeline_settings)) 135 | f.close() 136 | 137 | def load_timeline_settings(): 138 | global confpath 139 | global timeline_settings 140 | try: 141 | f=open(confpath+"/timelinecache","rb") 142 | timeline_settings=pickle.loads(f.read()) 143 | f.close() 144 | except: 145 | return False 146 | 147 | def get_timeline_settings(account_id,name): 148 | global timeline_settings 149 | for i in timeline_settings: 150 | if i.tl==name and i.account_id==account_id: 151 | return i 152 | timeline_settings.append(timeline.TimelineSettings(account_id,name)) 153 | return timeline_settings[len(timeline_settings)-1] 154 | 155 | def clean_users(): 156 | global users 157 | users=[] -------------------------------------------------------------------------------- /keymac.keymap: -------------------------------------------------------------------------------- 1 | control+alt+shift+t=Tweet 2 | alt+shift+[=prev_tl 3 | alt+shift+]=next_tl 4 | alt+shift+;=prev_item 5 | alt+shift+'=next_item 6 | alt+shift+r=Reply 7 | alt+control+shift+r=Retweet 8 | alt+shift+l=Like 9 | alt+control+shift+d=Message 10 | alt+control+shift+pageup=Prev -------------------------------------------------------------------------------- /keymap.keymap: -------------------------------------------------------------------------------- 1 | control+win+t=Tweet 2 | alt+win+left=prev_tl 3 | alt+win+right=next_tl 4 | alt+win+up=prev_item 5 | alt+win+down=next_item 6 | control+win+pageup=prev_item_jump 7 | control+win+pagedown=next_item_jump 8 | control+win+r=Reply 9 | control+win+shift+r=Retweet 10 | alt+win+l=Like 11 | alt+win+q=Quote 12 | alt+win+control+d=Message 13 | alt+win+c=Conversation 14 | alt+win+v=View 15 | alt+win+[=Followers 16 | alt+win+]=Friends 17 | alt+win+control+up=Volup 18 | alt+win+control+down=Voldown 19 | alt+win+return=Url 20 | alt+win+;=SpeakUser 21 | alt+win+shift+;=SpeakReply 22 | alt+win+pageup=Prev 23 | alt+win+u=UserTimeline 24 | alt+win+shift+u=UserProfile 25 | alt+win+'=CloseTimeline 26 | alt+control+win+u=refresh 27 | alt+win+home=top_item 28 | alt+win+end=bottom_item 29 | alt+win+shift+return=PlayExternal 30 | alt+win+delete=Delete 31 | alt+win+o=Options 32 | control+alt+win+o=AccountOptions 33 | alt+win+shift+left=previous_from_user 34 | alt+win+shift+right=next_from_user 35 | alt+win+shift+up=previous_in_thread 36 | alt+win+shift+down=next_in_thread 37 | control+win+shift+c=Copy 38 | alt+win+a=AddToList 39 | alt+win+shift+a=RemoveFromList 40 | alt+win+control+l=Lists 41 | alt+win+/=Search 42 | alt+win+shift+/=UserSearch 43 | control+win+a=Accounts 44 | alt+win+space=speak_item 45 | alt+win+control+a=speak_account 46 | control+alt+shift+win+return=TweetUrl 47 | alt+win+shift+e=Read 48 | alt+win+shift+m=Mute -------------------------------------------------------------------------------- /nvdaControllerClient64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/nvdaControllerClient64.dll -------------------------------------------------------------------------------- /quinter.pyw: -------------------------------------------------------------------------------- 1 | import application 2 | import platform 3 | import sys 4 | sys.dont_write_bytecode=True 5 | if platform.system()!="Darwin": 6 | f=open("errors.log","a") 7 | sys.stderr=f 8 | import shutil 9 | import os 10 | if os.path.exists(os.path.expandvars("%temp%\gen_py")): 11 | shutil.rmtree(os.path.expandvars("%temp%\gen_py")) 12 | # Bye foo! 13 | import wx 14 | app = wx.App(redirect=False) 15 | 16 | import speak 17 | from GUI import main 18 | import globals 19 | globals.load() 20 | if globals.prefs.window_shown: 21 | main.window.Show() 22 | else: 23 | speak.speak("Welcome to Quinter! Main window hidden.") 24 | import utils 25 | app.MainLoop() -------------------------------------------------------------------------------- /quinter_updater/unzip.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/quinter_updater/unzip.exe -------------------------------------------------------------------------------- /quinter_updater/updater.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/quinter_updater/updater.exe -------------------------------------------------------------------------------- /quinter_updater/updater.pb: -------------------------------------------------------------------------------- 1 | RunProgram("taskkill.exe","-f -im Quinter.exe","",#PB_Program_Wait|#PB_Program_Hide) 2 | RunProgram("unzip.exe","-o -qq Quinter.zip","",#PB_Program_Wait|#PB_Program_Hide) 3 | RunProgram("Quinter.exe","","") 4 | ; IDE Options = PureBasic 5.73 LTS (Windows - x64) 5 | ; CursorPosition = 2 6 | ; EnableThread 7 | ; EnableXP 8 | ; EnableAdmin 9 | ; Executable = updater.exe 10 | ; EnableCompileCount = 3 11 | ; EnableBuildCount = 3 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tweepy 2 | git+https://www.github.com/accessibleapps/sound_lib 3 | tweak 4 | nuitka 5 | git+https://www.github.com/accessibleapps/accessible_output2 6 | git+https://www.github.com/accessibleapps/keyboard_handler 7 | pyperclip 8 | wxpython 9 | twitter-text-parser 10 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | python quinter.pyw 3 | pause 4 | -------------------------------------------------------------------------------- /sound.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sound_lib 3 | from sound_lib import stream 4 | from sound_lib import output as o 5 | import globals 6 | import speak 7 | import re 8 | 9 | out = o.Output() 10 | handle = None 11 | 12 | def return_url(url): 13 | return url 14 | 15 | media_matchlist = [ 16 | {"match": r"https://sndup.net/[a-zA-Z0-9]+/[ad]$", "func":return_url}, 17 | {"match": r"^http:\/\/\S+(\/\S+)*(\/)?\.(mp3|m4a|ogg|opus|flac)$", "func":return_url}, 18 | {"match": r"^https:\/\/\S+(\/\S+)*(\/)?\.(mp3|m4a|ogg|opus|flac)$", "func":return_url}, 19 | {"match": r"^http:\/\/\S+:[+-]?[1-9]\d*|0(\/\S+)*(\/)?$", "func":return_url}, 20 | {"match": r"^https:\/\/\S+:[+-]?[1-9]\d*|0(\/\S+)*(\/)?$", "func":return_url}, 21 | {"match": r"https?://twitter.com/.+/status/.+/video/.+", "func":return_url}, 22 | {"match": r"https?://twitch.tv/.", "func":return_url}, 23 | {"match": r"http?://twitch.tv/.", "func":return_url}, 24 | {"match": r"https?://vm.tiktok.com/.+", "func":return_url}, 25 | {"match": r"https?://soundcloud.com/.+", "func":return_url}, 26 | {"match": r"https?://t.co/.", "func":return_url}, 27 | {"match": r"^(?:https?:\/\/)?(?:m\.|www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$", "func":return_url} 28 | ] 29 | 30 | def get_media_urls(urls): 31 | result = [] 32 | for u in urls: 33 | for service in media_matchlist: 34 | if re.match(service['match'], u.lower()) != None: 35 | result.append({"url":u, "func":service['func']}) 36 | return result 37 | 38 | def play(account, filename, pack="", wait=False): 39 | global handle 40 | if handle != None: 41 | try: 42 | handle.stop() 43 | except sound_lib.main.BassError: 44 | pass 45 | try: 46 | handle.free() 47 | except sound_lib.main.BassError: 48 | pass 49 | if os.path.exists(globals.confpath + "/sounds/" + account.prefs.soundpack + "/" + filename + ".ogg"): 50 | path = globals.confpath + "/sounds/" + account.prefs.soundpack + "/" + filename + ".ogg" 51 | elif os.path.exists("sounds/" + account.prefs.soundpack + "/" + filename + ".ogg"): 52 | path = "sounds/" + account.prefs.soundpack + "/" + filename + ".ogg" 53 | elif os.path.exists(globals.confpath + "/sounds/default/" + filename + ".ogg"): 54 | path=globals.confpath+"/sounds/default/" + filename + ".ogg" 55 | elif os.path.exists("sounds/default/"+filename + ".ogg"): 56 | path="sounds/default/" + filename + ".ogg" 57 | else: 58 | return 59 | try: 60 | handle = stream.FileStream(file=path) 61 | handle.pan=account.prefs.soundpan 62 | handle.volume = globals.prefs.volume 63 | handle.looping = False 64 | if wait: 65 | handle.play_blocking() 66 | else: 67 | handle.play() 68 | except sound_lib.main.BassError: 69 | pass 70 | -------------------------------------------------------------------------------- /sounds/default/boundary.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/boundary.ogg -------------------------------------------------------------------------------- /sounds/default/close.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/close.ogg -------------------------------------------------------------------------------- /sounds/default/delete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/delete.ogg -------------------------------------------------------------------------------- /sounds/default/error.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/error.ogg -------------------------------------------------------------------------------- /sounds/default/follow.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/follow.ogg -------------------------------------------------------------------------------- /sounds/default/home.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/home.ogg -------------------------------------------------------------------------------- /sounds/default/like.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/like.ogg -------------------------------------------------------------------------------- /sounds/default/likes.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/likes.ogg -------------------------------------------------------------------------------- /sounds/default/list.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/list.ogg -------------------------------------------------------------------------------- /sounds/default/max_length.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/max_length.ogg -------------------------------------------------------------------------------- /sounds/default/media.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/media.ogg -------------------------------------------------------------------------------- /sounds/default/mentions.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/mentions.ogg -------------------------------------------------------------------------------- /sounds/default/messages.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/messages.ogg -------------------------------------------------------------------------------- /sounds/default/new.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/new.ogg -------------------------------------------------------------------------------- /sounds/default/open.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/open.ogg -------------------------------------------------------------------------------- /sounds/default/ready.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/ready.ogg -------------------------------------------------------------------------------- /sounds/default/search.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/search.ogg -------------------------------------------------------------------------------- /sounds/default/send_message.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/send_message.ogg -------------------------------------------------------------------------------- /sounds/default/send_reply.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/send_reply.ogg -------------------------------------------------------------------------------- /sounds/default/send_retweet.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/send_retweet.ogg -------------------------------------------------------------------------------- /sounds/default/send_tweet.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/send_tweet.ogg -------------------------------------------------------------------------------- /sounds/default/unfollow.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/unfollow.ogg -------------------------------------------------------------------------------- /sounds/default/unlike.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/unlike.ogg -------------------------------------------------------------------------------- /sounds/default/user.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/user.ogg -------------------------------------------------------------------------------- /sounds/default/volume_changed.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuinterApp/Quinter/c73ca87f77584be5ccefda1f00a9ceffb535aba7/sounds/default/volume_changed.ogg -------------------------------------------------------------------------------- /speak.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | if platform.system() == "Darwin": 4 | from accessible_output2 import outputs 5 | speaker = outputs.auto.Auto() 6 | else: 7 | import Tolk as speaker 8 | speaker.load() 9 | speaker.try_sapi = True 10 | 11 | def speak(text,interrupt=False): 12 | if platform.system() == "Darwin": 13 | speaker.speak(text, interrupt) 14 | else: 15 | speaker.output(text, interrupt) 16 | -------------------------------------------------------------------------------- /streaming.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from tweepy.models import Status 3 | import tweepy 4 | from GUI import main 5 | import globals 6 | 7 | import time 8 | import speak 9 | 10 | import sys 11 | import utils 12 | 13 | from tweepy import TweepyException 14 | class StreamListener(tweepy.Stream): 15 | 16 | def __init__(self, account, *args, **kwargs): 17 | super(StreamListener, self).__init__(*args, **kwargs) 18 | self.account = account 19 | try: 20 | self.users = [str(id) for id in self.account.api.get_friend_ids()] 21 | except TweepyException as e: 22 | utils.handle_error(e) 23 | self.users=[] 24 | muted=self.account.api.get_mutes() 25 | for i in muted: 26 | if i.id_str in self.users: 27 | self.users.remove(i.id_str) 28 | self.users.append(str(self.account.me.id)) 29 | self.home_users=[] 30 | for i in self.users: 31 | self.home_users.append(i) 32 | for i in self.account.timelines: 33 | if i.type=="user" and not i.user.protected and i.user.id_str not in self.users: 34 | self.users.append(i.user.id_str) 35 | 36 | def on_connect(self): 37 | speak.speak("Streaming started for "+self.account.me.screen_name) 38 | 39 | def on_exception(self, ex): 40 | speak.speak("Exception in stream for "+self.account.me.screen_name) 41 | 42 | def on_status(self, status): 43 | """ Checks data arriving as a tweet. """ 44 | send_home=True 45 | if status.in_reply_to_user_id_str != None and status.in_reply_to_user_id_str not in self.users: 46 | send_home=False 47 | if status.user.id_str not in self.home_users: 48 | send_home=False 49 | if hasattr(status, "retweeted_status"): 50 | if hasattr(status.retweeted_status, "extended_tweet"): 51 | status.retweeted_status._json = {**status.retweeted_status._json, **status.retweeted_status._json["extended_tweet"]} 52 | status.retweeted_status=Status().parse(None,status.retweeted_status._json) 53 | if hasattr(status, "quoted_status"): 54 | if hasattr(status.quoted_status, "extended_tweet"): 55 | status.quoted_status._json = {**status.quoted_status._json, **status.quoted_status._json["extended_tweet"]} 56 | status.quoted_status=Status().parse(None,status.quoted_status._json) 57 | if status.truncated: 58 | status._json = {**status._json, **status._json["extended_tweet"]} 59 | status=Status().parse(None,status._json) 60 | if status.user.id_str in self.users: 61 | if send_home: 62 | self.account.timelines[0].load(items=[status]) 63 | if status.user.screen_name!=self.account.me.screen_name: 64 | users=utils.get_user_objects_in_tweet(self.account,status) 65 | for i in users: 66 | if i.screen_name==self.account.me.screen_name: 67 | self.account.timelines[1].load(items=[status]) 68 | if status.user.id==self.account.me.id: 69 | self.account.timelines[4].load(items=[status]) 70 | for i in self.account.timelines: 71 | if i.type=="list" and status.user.id in i.members: 72 | i.load(items=[status]) 73 | if i.type=="user" and status.user.screen_name==i.data: 74 | i.load(items=[status]) -------------------------------------------------------------------------------- /timeline.py: -------------------------------------------------------------------------------- 1 | from tweepy import TweepyException 2 | import time 3 | import globals 4 | import utils 5 | import speak 6 | import sound 7 | import threading 8 | import os 9 | from GUI import main 10 | class TimelineSettings(object): 11 | def __init__(self,account,tl): 12 | self.account_id=account 13 | self.tl=tl 14 | self.mute=False 15 | self.read=False 16 | self.hide=False 17 | 18 | class timeline(object): 19 | def __init__(self,account,name,type,data=None,user=None,status=None,silent=False): 20 | self.members=[] 21 | self.account=account 22 | self.status=status 23 | self.name=name 24 | self.removable=False 25 | self.initial=True 26 | self.statuses=[] 27 | self.type=type 28 | self.data=data 29 | self.user=user 30 | self.index=0 31 | self.page=0 32 | self.mute=False 33 | self.read=False 34 | self.hide=False 35 | for i in globals.timeline_settings: 36 | if i.account_id==self.account.me.id and i.tl==self.name: 37 | self.mute=i.mute 38 | self.read=i.read 39 | self.hide=i.hide 40 | if self.type=="user" and self.name!="Sent" or self.type=="conversation" or self.type=="search" or self.type=="list": 41 | if not silent: 42 | sound.play(self.account,"open") 43 | self.removable=True 44 | if self.type!="messages": 45 | self.update_kwargs={"count":globals.prefs.count,"tweet_mode":'extended'} 46 | self.prev_kwargs={"count":globals.prefs.count,"tweet_mode":'extended'} 47 | else: 48 | self.update_kwargs={"count":50} 49 | if self.type=="home": 50 | self.func=self.account.api.home_timeline 51 | elif self.type=="mentions": 52 | self.func=self.account.api.mentions_timeline 53 | elif self.type=="messages": 54 | self.func=self.account.api.get_direct_messages 55 | elif self.type=="likes": 56 | self.func=self.account.api.get_favorites 57 | elif self.type=="user": 58 | self.func=self.account.api.user_timeline 59 | self.update_kwargs['id']=self.data 60 | self.prev_kwargs['id']=self.data 61 | elif self.type=="list": 62 | self.func=self.account.api.list_timeline 63 | self.update_kwargs['list_id']=self.data 64 | self.prev_kwargs['list_id']=self.data 65 | elif self.type=="search": 66 | self.func=self.account.api.search_tweets 67 | self.update_kwargs['q']=self.data 68 | self.prev_kwargs['q']=self.data 69 | if self.type!="conversation": 70 | threading.Thread(target=self.load,daemon=True).start() 71 | else: 72 | self.load_conversation() 73 | if self.type=="messages": 74 | m=globals.load_messages(self.account) 75 | if m!=None: 76 | self.statuses=m 77 | self.initial=False 78 | 79 | def read_items(self,items): 80 | pref="" 81 | if len(globals.accounts)>1: 82 | pref=self.account.me.screen_name+": " 83 | if len(items)>=4: 84 | speak.speak(pref+str(len(items))+" new in "+self.name) 85 | return 86 | speak.speak(pref+", ".join(self.prepare(items))) 87 | 88 | def load_conversation(self): 89 | status=self.status 90 | self.process_status(status) 91 | if globals.prefs.reversed: 92 | self.statuses.reverse() 93 | if self.account.currentTimeline==self: 94 | main.window.refreshList() 95 | sound.play(self.account,"search") 96 | 97 | def play(self): 98 | if self.type=="user": 99 | if not os.path.exists("sounds/"+self.account.prefs.soundpack+"/"+self.user.screen_name+".ogg"): 100 | sound.play(self.account,"user") 101 | else: 102 | sound.play(self.account,self.user.screen_name) 103 | else: 104 | if self.type=="search": 105 | sound.play(self.account,"search") 106 | elif self.type=="list": 107 | sound.play(self.account,"list") 108 | else: 109 | sound.play(self.account,self.name) 110 | 111 | def process_status(self,status): 112 | self.statuses.append(status) 113 | try: 114 | if hasattr(status,"in_reply_to_status_id") and status.in_reply_to_status_id!=None: 115 | self.process_status(utils.lookup_status(self.account,status.in_reply_to_status_id)) 116 | if hasattr(status,"retweeted_status"): 117 | self.process_status(status.retweeted_status) 118 | if hasattr(status,"quoted_status"): 119 | self.process_status(status.quoted_status) 120 | except: 121 | pass 122 | 123 | def hide_tl(self): 124 | if self.type=="user" and self.name!="Sent" or self.type=="list" or self.type=="search" or self.type=="conversation": 125 | utils.alert("You can't hide this timeline. Try closing it instead.","Error") 126 | return 127 | self.hide=True 128 | globals.get_timeline_settings(self.account.me.id,self.name).hide=self.hide 129 | globals.save_timeline_settings() 130 | if self.account.currentTimeline==self: 131 | self.account.currentTimeline=self.account.timelines[0] 132 | main.window.refreshTimelines() 133 | 134 | def unhide_tl(self): 135 | self.hide=False 136 | globals.get_timeline_settings(self.account.me.id,self.name).hide=self.hide 137 | globals.save_timeline_settings() 138 | main.window.refreshTimelines() 139 | 140 | def load(self,back=False,speech=False,items=[]): 141 | if self.hide: 142 | return False 143 | if items==[]: 144 | if back: 145 | if not globals.prefs.reversed: 146 | self.prev_kwargs['max_id']=self.statuses[len(self.statuses)-1].id 147 | else: 148 | self.prev_kwargs['max_id']=self.statuses[0].id 149 | tl=None 150 | try: 151 | if not back: 152 | tl=self.func(**self.update_kwargs) 153 | else: 154 | tl=self.func(**self.prev_kwargs) 155 | except TweepyException as error: 156 | utils.handle_error(error,self.account.me.screen_name+"'s "+self.name) 157 | if self.removable: 158 | if self.type=="user" and self.data in self.account.prefs.user_timelines: 159 | self.account.prefs.user_timelines.remove(self.data) 160 | if self.type=="list" and self.data in self.account.prefs.list_timelines: 161 | self.account.prefs.list_timelines.remove(self.data) 162 | if self.type=="search" and self.data in self.account.prefs.search_timelines: 163 | self.account.prefs.search_timelines.remove(self.data) 164 | self.account.timelines.remove(self) 165 | if self.account==globals.currentAccount: 166 | main.window.refreshTimelines() 167 | if self.account.currentTimeline==self: 168 | main.window.list.SetSelection(0) 169 | self.account.currentIndex=0 170 | main.window.on_list_change(None) 171 | 172 | return 173 | else: 174 | tl=items 175 | if tl!=None: 176 | newitems=0 177 | objs=[] 178 | objs2=[] 179 | for i in tl: 180 | if self.type!="messages": 181 | utils.add_users(i) 182 | if not utils.isDuplicate(i, self.statuses): 183 | newitems+=1 184 | if self.initial or back: 185 | if not globals.prefs.reversed: 186 | self.statuses.append(i) 187 | objs2.append(i) 188 | else: 189 | self.statuses.insert(0,i) 190 | objs2.insert(0,i) 191 | else: 192 | if not globals.prefs.reversed: 193 | objs.append(i) 194 | objs2.append(i) 195 | else: 196 | objs.insert(0,i) 197 | objs2.insert(0,i) 198 | 199 | if newitems==0 and speech: 200 | speak.speak("Nothing new.") 201 | if newitems>0: 202 | if self.read: 203 | self.read_items(objs2) 204 | if len(objs)>0: 205 | if not globals.prefs.reversed: 206 | objs.reverse() 207 | objs2.reverse() 208 | for i in objs: 209 | if not globals.prefs.reversed: 210 | self.statuses.insert(0,i) 211 | else: 212 | self.statuses.append(i) 213 | 214 | if globals.currentAccount==self.account and self.account.currentTimeline==self: 215 | if not back and not self.initial: 216 | if not globals.prefs.reversed: 217 | main.window.add_to_list(self.prepare(objs2)) 218 | else: 219 | objs2.reverse() 220 | main.window.append_to_list(self.prepare(objs2)) 221 | 222 | else: 223 | if not globals.prefs.reversed: 224 | main.window.append_to_list(self.prepare(objs2)) 225 | else: 226 | main.window.add_to_list(self.prepare(objs2)) 227 | 228 | if items==[] and self.type!="messages": 229 | if not globals.prefs.reversed: 230 | self.update_kwargs['since_id']=tl[0].id 231 | else: 232 | self.update_kwargs['since_id']=tl[len(tl)-1].id 233 | 234 | if not back and not self.initial: 235 | if not globals.prefs.reversed: 236 | self.index+=newitems 237 | if globals.currentAccount==self.account and self.account.currentTimeline==self and len(self.statuses)>0: 238 | try: 239 | main.window.list2.SetSelection(self.index) 240 | except: 241 | pass 242 | if back and globals.prefs.reversed: 243 | self.index+=newitems 244 | if globals.currentAccount==self.account and self.account.currentTimeline==self and len(self.statuses)>0: 245 | main.window.list2.SetSelection(self.index) 246 | 247 | if self.initial: 248 | if not globals.prefs.reversed: 249 | self.index=0 250 | else: 251 | self.index=len(self.statuses)-1 252 | if not self.mute and not self.hide: 253 | self.play() 254 | globals.prefs.statuses_received+=newitems 255 | if speech: 256 | announcement=f"{newitems} new item" 257 | if newitems!=1: 258 | announcement+="s" 259 | speak.speak(announcement) 260 | if self.initial: 261 | self.initial=False 262 | # if globals.currentTimeline==self: 263 | # main.window.refreshList() 264 | if self.type=="messages": 265 | globals.save_messages(self.account,self.statuses) 266 | if self == self.account.timelines[len(self.account.timelines) - 1] and not self.account.ready: 267 | self.account.ready=True 268 | sound.play(self.account,"ready") 269 | 270 | def toggle_read(self): 271 | if self.read: 272 | self.read=False 273 | speak.speak("Autoread off") 274 | else: 275 | self.read=True 276 | speak.speak("Autoread on") 277 | globals.get_timeline_settings(self.account.me.id,self.name).read=self.read 278 | globals.save_timeline_settings() 279 | 280 | def toggle_mute(self): 281 | if self.mute: 282 | self.mute=False 283 | speak.speak("Unmuted") 284 | else: 285 | self.mute=True 286 | speak.speak("Muted") 287 | globals.get_timeline_settings(self.account.me.id,self.name).mute=self.mute 288 | globals.save_timeline_settings() 289 | 290 | def get(self): 291 | items=[] 292 | for i in self.statuses: 293 | if self.type!="messages": 294 | items.append(utils.process_tweet(i)) 295 | else: 296 | items.append(utils.process_message(i)) 297 | return items 298 | 299 | def prepare(self,items): 300 | items2=[] 301 | for i in items: 302 | if self.type!="messages": 303 | if not globals.prefs.reversed: 304 | items2.append(utils.process_tweet(i)) 305 | else: 306 | items2.insert(0,utils.process_tweet(i)) 307 | else: 308 | if not globals.prefs.reversed: 309 | items2.append(utils.process_message(i)) 310 | else: 311 | items2.insert(0,utils.process_message(i)) 312 | return items2 313 | 314 | def add(account,name,type,data=None,user=None): 315 | account.timelines.append(timeline(account,name,type,data,user)) 316 | if account==globals.currentAccount: 317 | main.window.refreshTimelines() 318 | 319 | def timelineThread(account): 320 | while 1: 321 | time.sleep(globals.prefs.update_time*60) 322 | for i in account.timelines: 323 | try: 324 | if i.type=="list": 325 | try: 326 | members=account.api.get_list_members(list_id=i.data) 327 | i.members=[] 328 | for i2 in members: 329 | i.members.append(i2.id) 330 | except: 331 | pass 332 | if i.type!="conversation": 333 | i.load() 334 | except TweepyException as error: 335 | sound.play(account,"error") 336 | if hasattr(error,"response"): 337 | speak.speak(error.response.text) 338 | else: 339 | speak.speak(str(error)) 340 | if globals.prefs.streaming and (account.stream != None and not account.stream.running or account.stream == None): 341 | account.start_stream() 342 | if len(globals.unknown_users)>0: 343 | try: 344 | new_users=account.api.lookup_users(user_id=globals.unknown_users) 345 | for i in new_users: 346 | if i not in globals.users: 347 | globals.users.insert(0,i) 348 | globals.unknown_users=[] 349 | except: 350 | globals.unknown_users=[] 351 | 352 | globals.save_users() 353 | def reverse(): 354 | for i in globals.accounts: 355 | for i2 in i.timelines: 356 | i2.statuses.reverse() 357 | i2.index=(len(i2.statuses)-1)-i2.index 358 | main.window.on_list_change(None) -------------------------------------------------------------------------------- /twishort.py: -------------------------------------------------------------------------------- 1 | from http.client import HTTPConnection 2 | from urllib.parse import urlparse 3 | import requests 4 | 5 | key = "7233245e498f12569d29dc8910ee5cb2" 6 | 7 | def get_twishort_uri(url): 8 | try: 9 | return url.split("twishort.com/")[1] 10 | except IndexError: 11 | return "" 12 | 13 | def get_full_text(uri): 14 | r = requests.get("http://api.twishort.com/1.1/get.json", params={"uri": uri, "api_key": key}) 15 | return r.json()["text"] 16 | 17 | def unshorten(url): 18 | working = urlparse(url) 19 | if not working.netloc: 20 | raise TypeError("Unable to parse URL.") 21 | con = HTTPConnection(working.netloc) 22 | con.connect() 23 | con.request("GET", working.path) 24 | resp = con.getresponse() 25 | con.close() 26 | return resp.getheader("location") 27 | -------------------------------------------------------------------------------- /twitter.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import tweepy 3 | from tweepy import TweepyException 4 | import streaming 5 | import application 6 | import utils 7 | import threading 8 | from GUI import main, misc 9 | import tweak 10 | import timeline 11 | import speak 12 | from GUI.ask import * 13 | import webbrowser 14 | import platform 15 | import os 16 | import sys 17 | 18 | import globals 19 | import sound 20 | API_KEY = "xyTlkymHxgbfjasI2OL0O2ssG" 21 | API_SECRET = "sLKYWCCZw5zX6xR2K04enB0TAQLTwCsCHuQIEhZT4KHhkCM6zW" 22 | 23 | class twitter(object): 24 | def __init__(self,index): 25 | self.stream_thread=None 26 | self.ready=False 27 | self.timelines=[] 28 | self.currentTimeline=None 29 | self.currentIndex=0 30 | self.currentStatus=None 31 | self.confpath="" 32 | self.prefs=tweak.Config(name="Quinter/account"+str(index),autosave=True) 33 | self.confpath=self.prefs.user_config_dir 34 | self.prefs.key=self.prefs.get("key","") 35 | self.prefs.secret=self.prefs.get("secret","") 36 | self.prefs.user_timelines=self.prefs.get("user_timelines",[]) 37 | self.prefs.list_timelines=self.prefs.get("list_timelines",[]) 38 | self.prefs.search_timelines=self.prefs.get("search_timelines",[]) 39 | self.prefs.follow_prompt=self.prefs.get("follow_prompt",False) 40 | self.prefs.footer=self.prefs.get("footer","") 41 | self.prefs.soundpack=self.prefs.get("soundpack","default") 42 | self.prefs.soundpan=self.prefs.get("soundpan",0) 43 | self.auth=tweepy.OAuthHandler(API_KEY, API_SECRET) 44 | if self.prefs.key==None or self.prefs.secret==None or self.prefs.key=="" or self.prefs.secret=="": 45 | if platform.system()!="Darwin": 46 | webbrowser.open(self.auth.get_authorization_url()) 47 | else: 48 | os.system("open "+self.auth.get_authorization_url()) 49 | verifier = ask(caption="Pin",message='Enter pin:') 50 | if verifier==None: 51 | sys.exit() 52 | self.auth.get_access_token(verifier) 53 | self.prefs.key=self.auth.access_token 54 | self.prefs.secret=self.auth.access_token_secret 55 | else: 56 | self.auth.set_access_token(self.prefs.key,self.prefs.secret) 57 | self.api = tweepy.API(self.auth) 58 | self.api2 = tweepy.Client(consumer_key=API_KEY,consumer_secret=API_SECRET,access_token=self.prefs.key,access_token_secret=self.prefs.secret) 59 | self.me=self.api.verify_credentials() 60 | if globals.currentAccount==None: 61 | globals.currentAccount=self 62 | main.window.SetLabel(self.me.screen_name+" - "+application.name+" "+application.version) 63 | timeline.add(self,"Home","home") 64 | timeline.add(self,"Mentions","mentions") 65 | timeline.add(self,"Messages","messages") 66 | timeline.add(self,"Likes","likes") 67 | timeline.add(self,"Sent","user",self.me.screen_name,self.me) 68 | for i in self.prefs.user_timelines: 69 | tl=misc.user_timeline_user(self,i,False) 70 | if not tl: 71 | self.prefs.user_timelines.remove(i) 72 | for i in self.prefs.list_timelines: 73 | misc.list_timeline(self,self.api.get_list(list_id=i).name,i,False) 74 | for i in self.prefs.search_timelines: 75 | misc.search(self,i,False) 76 | self.stream_listener=None 77 | self.stream=None 78 | if globals.prefs.streaming: 79 | self.start_stream() 80 | 81 | if globals.currentAccount==self: 82 | main.window.list.SetSelection(0) 83 | main.window.on_list_change(None) 84 | threading.Thread(target=timeline.timelineThread,args=[self,],daemon=True).start() 85 | if not self.prefs.follow_prompt: 86 | q=utils.question("Follow for app updates and support","Would you like to follow @QuinterApp to get app updates and support?") 87 | if q==1: 88 | misc.follow_user(self,"@QuinterApp") 89 | self.prefs.follow_prompt=True 90 | 91 | def start_stream(self): 92 | if self.stream_listener==None: 93 | self.stream_listener = streaming.StreamListener(self,API_KEY,API_SECRET,self.prefs.key,self.prefs.secret) 94 | self.stream_thread=threading.Thread(target=self.stream_listener.filter, kwargs={"follow":self.stream_listener.users},daemon=True) 95 | self.stream_thread.start() 96 | 97 | def followers(self,id): 98 | count=0 99 | cursor=-1 100 | followers=[] 101 | try: 102 | f=self.api.get_followers(id=id,cursor=cursor,count=200) 103 | except TweepyException as err: 104 | utils.handle_error(err,"followers") 105 | return [] 106 | for i in f[0]: 107 | followers.append(i) 108 | cursor=f[1][1] 109 | count+=1 110 | while len(f)>0: 111 | if count>=globals.prefs.user_limit: 112 | return followers 113 | try: 114 | f=self.api.get_followers(id=id,cursor=cursor,count=200) 115 | count+=1 116 | except TweepyException as err: 117 | utils.handle_error(err,"followers") 118 | return followers 119 | if len(f[0])==0: 120 | return followers 121 | for i in f[0]: 122 | followers.append(i) 123 | cursor=f[1][1] 124 | if cursor<1000: 125 | return followers 126 | return followers 127 | 128 | def friends(self,id): 129 | count=0 130 | cursor=-1 131 | followers=[] 132 | try: 133 | f=self.api.get_friends(id=id,cursor=cursor,count=200) 134 | except TweepyException as err: 135 | utils.handle_error(err,"friends") 136 | return [] 137 | for i in f[0]: 138 | followers.append(i) 139 | count+=1 140 | cursor=f[1][1] 141 | while len(f)>0: 142 | if count>=globals.prefs.user_limit: 143 | return followers 144 | try: 145 | f=self.api.get_friends(id=id,cursor=cursor,count=200) 146 | count+=1 147 | except TweepyException as err: 148 | utils.handle_error(err,"friends") 149 | return followers 150 | if len(f[0])==0: 151 | return followers 152 | for i in f[0]: 153 | followers.append(i) 154 | cursor=f[1][1] 155 | if cursor<1000: 156 | return followers 157 | return followers 158 | 159 | def mutual_following(self): 160 | followers=self.followers(self.me.id) 161 | friends=self.friends(self.me.id) 162 | users=[] 163 | for i in friends: 164 | if i in followers: 165 | users.append(i) 166 | return users 167 | 168 | def not_following(self): 169 | followers=self.followers(self.me.id) 170 | users=[] 171 | for i in followers: 172 | if not i.following: 173 | users.append(i) 174 | return users 175 | 176 | def not_following_me(self): 177 | followers=self.followers(self.me.id) 178 | friends=self.friends(self.me.id) 179 | users=[] 180 | for i in friends: 181 | if not i in followers: 182 | users.append(i) 183 | return users 184 | 185 | def havent_tweeted(self): 186 | friends=self.friends(self.me.id) 187 | users=[] 188 | for i in friends: 189 | if hasattr(i,"status") and i.status.created_at.year]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?]))") 20 | url_re2=re.compile("(?:\w+://|www\.)[^ ,.?!#%=+][^ ]*") 21 | bad_chars="'\\.,[](){}:;\"" 22 | 23 | def process_tweet(s,return_only_text=False,template=""): 24 | if hasattr(s,"message_create"): 25 | print("We don't support this in this function, are you drunk?") 26 | return 27 | if hasattr(s,"extended_tweet") and "full_text" in s.extended_tweet: 28 | text=html.unescape(s.extended_tweet['full_text']) 29 | else: 30 | if hasattr(s,"full_text"): 31 | text=html.unescape(s.full_text) 32 | else: 33 | text=html.unescape(s.text) 34 | if hasattr(s,"entities")!=False: 35 | if "urls" in s.entities!=False: 36 | for url in s.entities['urls']: 37 | try: 38 | text=text.replace(url['url'],url['expanded_url']) 39 | if url['expanded_url'] not in text: 40 | text+=" "+url['expanded_url'] 41 | except IndexError: 42 | pass 43 | 44 | if hasattr(s,"extended_tweet") and "urls" in s.extended_tweet['entities']: 45 | for url in s.extended_tweet['entities']['urls']: 46 | try: 47 | text=text.replace(url['url'],url['expanded_url']) 48 | if url['expanded_url'] not in text: 49 | text+=" "+url['expanded_url'] 50 | except IndexError: 51 | pass 52 | 53 | if hasattr(s,"retweeted_status")!=False: 54 | qs=s.retweeted_status 55 | text=process_tweet(qs,False,globals.prefs.retweetTemplate) 56 | 57 | urls=find_urls_in_text(text) 58 | for url in range(0,len(urls)): 59 | if "twitter.com/i/web" in urls[url]: 60 | text=text.replace(urls[url],"") 61 | if hasattr(s,"quoted_status")!=False: 62 | qs=s.quoted_status 63 | urls=find_urls_in_text(text) 64 | for url in range(0,len(urls)): 65 | if "twitter.com" in urls[url]: 66 | text=text.replace(urls[url],"") 67 | text+=process_tweet(qs,False,globals.prefs.quoteTemplate) 68 | if not process_tweet(qs,False,globals.prefs.quoteTemplate) in text: 69 | text+=process_tweet(qs,False,globals.prefs.quoteTemplate) 70 | 71 | s.text=text 72 | if not return_only_text: 73 | return template_to_string(s,template) 74 | else: 75 | return text 76 | 77 | def process_message(s, return_text=False): 78 | text=html.unescape(s.message_create['message_data']['text']) 79 | if "entities" in s.message_create['message_data']!=False: 80 | if "urls" in s.message_create["message_data"]["entities"]!=False: 81 | urls=find_urls_in_text(text) 82 | for url in range(0,len(urls)): 83 | try: 84 | text=text.replace(urls[url],s.message_create["message_data"]["entities"]['urls'][url]['expanded_url']) 85 | except IndexError: 86 | pass 87 | 88 | s.message_create["message_data"]["text"]=text 89 | if not return_text: 90 | return message_template_to_string(s) 91 | else: 92 | return s.message_create['message_data']['text'] 93 | 94 | def find_urls_in_text(text): 95 | return [s.strip(bad_chars) for s in url_re2.findall(text)] 96 | 97 | def find_urls_in_tweet(s): 98 | urls=[] 99 | if hasattr(s,"entities"): 100 | if s.entities['urls']!=[]: 101 | for i in s.entities['urls']: 102 | urls.append(i['expanded_url']) 103 | if "media" in s.entities: 104 | for i in s.entities['media']: 105 | urls.append(i['expanded_url']) 106 | return urls 107 | 108 | def template_to_string(s,template=""): 109 | if template=="": 110 | template=globals.prefs.tweetTemplate 111 | temp=template.split(" ") 112 | for i in range(len(temp)): 113 | if "$" in temp[i]: 114 | t=temp[i].split("$") 115 | r=t[1] 116 | if "." in r: 117 | q=r.split(".") 118 | o=q[0] 119 | p=q[1] 120 | 121 | if hasattr(s,o) and hasattr(getattr(s,o),p): 122 | try: 123 | if (o=="name" or p=="name") and globals.prefs.demojify: 124 | deEmojify=True 125 | else: 126 | deEmojify=False 127 | f1=getattr(s,o) 128 | if deEmojify: 129 | demojied=demojify(getattr(f1,p)) 130 | if demojied=="": 131 | template=template.replace("$"+t[1]+"$",getattr(f1,"screen_name")) 132 | else: 133 | template=template.replace("$"+t[1]+"$",demojied) 134 | else: 135 | template=template.replace("$"+t[1]+"$",getattr(f1,p)) 136 | except: 137 | try: 138 | f1=getattr(s,o) 139 | template=template.replace("$"+t[1]+"$",str(getattr(f1,p))) 140 | except Exception as e: 141 | print(e) 142 | 143 | else: 144 | if hasattr(s,t[1]): 145 | try: 146 | if t[1]=="name" and globals.prefs.demojify or t[1]=="text" and globals.prefs.demojify_tweet: 147 | deEmojify=True 148 | else: 149 | deEmojify=False 150 | if deEmojify: 151 | demojied=demojify(getattr(s,t[1])) 152 | if demojied=="" and t[1]=="name": 153 | template=template.replace("$"+t[1]+"$",getattr(s,"screen_name")) 154 | else: 155 | template=template.replace("$"+t[1]+"$",demojied) 156 | else: 157 | if t[1]=="created_at": 158 | template=template.replace("$"+t[1]+"$",parse_date(getattr(s,t[1]))) 159 | else: 160 | template=template.replace("$"+t[1]+"$",getattr(s,t[1])) 161 | except: 162 | try: 163 | template=template.replace("$"+t[1]+"$",str(getattr(s,t[1]))) 164 | except Exception as e: 165 | print(e) 166 | return template 167 | 168 | def message_template_to_string(s): 169 | s2={} 170 | template=globals.prefs.messageTemplate 171 | if "sender" not in s2: 172 | s2["sender"]=lookup_user(s.message_create['sender_id']) 173 | if "recipient" not in s2: 174 | s2["recipient"]=lookup_user(s.message_create['target']['recipient_id']) 175 | if globals.prefs.demojify: 176 | if s2['sender']!=None: 177 | s2['sender'].name=demojify(s2['sender'].name) 178 | if s2['sender'].name=="": 179 | s2['sender'].name=s2['sender'].screen_name 180 | if s2['recipient']!=None: 181 | s2['recipient.name']=demojify(s2['recipient'].name) 182 | if s2['recipient'].name=="": 183 | s2['recipient'].name=s2['recipient'].screen_name 184 | if "created_at" not in s2: 185 | s2['created_at']=parse_date(datetime.datetime.fromtimestamp(int(s.created_timestamp)/1000),False) 186 | temp=template.split(" ") 187 | for i in range(len(temp)): 188 | if "$" in temp[i]: 189 | t=temp[i].split("$") 190 | r=t[1] 191 | if "." in r: 192 | q=r.split(".") 193 | o=q[0] 194 | p=q[1] 195 | 196 | if o in s2 and type(s2[o])==dict and p in s2[o]: 197 | try: 198 | template=template.replace("$"+t[1]+"$",s2[o][p]) 199 | except Exception as e: 200 | print(e) 201 | 202 | elif o in s2 and hasattr(s2[o],p): 203 | try: 204 | attribute=getattr(s2[o],p) 205 | template=template.replace("$"+t[1]+"$",attribute) 206 | except Exception as e: 207 | print(e) 208 | 209 | elif o in s.message_create and p in s.message_create[o]: 210 | try: 211 | template=template.replace("$"+t[1]+"$",s.message_create[o][p]) 212 | except Exception as e: 213 | print(e) 214 | 215 | elif o in s.message_create['message_data'] and p in s.message_create['message_data'][o]: 216 | try: 217 | if t[1]=="text" and globals.prefs.demojify_tweet: 218 | demojified=demojify(s.message_create["message_data"][o][p]) 219 | template=template.replace("$"+t[1]+"$",demojified) 220 | else: 221 | template=template.replace("$"+t[1]+"$",s.message_create['message_data'][o][p]) 222 | except Exception as e: 223 | print(e) 224 | 225 | else: 226 | if t[1] in s2: 227 | try: 228 | template=template.replace("$"+t[1]+"$",s2[t[1]]) 229 | except Exception as e: 230 | print(e) 231 | elif t[1] in s.message_create: 232 | try: 233 | template=template.replace("$"+t[1]+"$",s.message_create[t[1]]) 234 | except Exception as e: 235 | print(e) 236 | elif t[1] in s.message_create['message_data']: 237 | try: 238 | if t[1]=="text" and globals.prefs.demojify_tweet: 239 | demojified=demojify(s.message_create["message_data"][t[1]]) 240 | template=template.replace("$"+t[1]+"$",demojified) 241 | else: 242 | template=template.replace("$"+t[1]+"$",s.message_create["message_data"][t[1]]) 243 | except Exception as e: 244 | print(e) 245 | return template 246 | 247 | def get_users_in_tweet(account,s): 248 | new="" 249 | 250 | if hasattr(s,"quoted_status")!=False and s.quoted_status.user.id!=account.me.id: 251 | s.text+=" "+s.quoted_status.user.screen_name 252 | if hasattr(s,"retweeted_status")!=False and s.retweeted_status.user.id!=account.me.id: 253 | s.text+=" "+s.retweeted_status.user.screen_name 254 | 255 | if s.user.screen_name!=account.me.screen_name: 256 | new=s.user.screen_name 257 | if hasattr(s,"entities") and "user_mentions" in s.entities: 258 | weew=s.entities['user_mentions'] 259 | for i in range(0,len(weew)): 260 | if account.me.screen_name!=weew[i]['screen_name']: 261 | new+=" "+weew[i]['screen_name'] 262 | return new 263 | 264 | def user(s): 265 | if s.has_key('user'): 266 | return s['user']['screen_name'] 267 | else: 268 | return s['sender']['screen_name'] 269 | 270 | def dict_match(d1, d2): 271 | for i in d2: 272 | if not i in d1: 273 | d1[i]=d2[i] 274 | return d1 275 | 276 | def class_match(d1, d2): 277 | names1=[p for p in dir(d1) if isinstance(getattr(d1,p),property)] 278 | names2=[p for p in dir(d2) if isinstance(getattr(d2,p),property)] 279 | for i in names2: 280 | if not i in names1: 281 | setattr(d1,i,getattr(d2,i,None)) 282 | return d1 283 | 284 | def parse_date(date,convert=True): 285 | ti=datetime.datetime.now() 286 | dst=time.localtime().tm_isdst 287 | if dst==1: 288 | tz=time.altzone 289 | else: 290 | tz=time.timezone 291 | if convert: 292 | try: 293 | date+=datetime.timedelta(seconds=0-tz) 294 | except: 295 | pass 296 | returnstring="" 297 | 298 | try: 299 | dateFormatString = "%m/%d/%Y" 300 | timeFormatString = "%I:%M:%S %p" 301 | if globals.prefs.use24HourTime: 302 | timeFormatString = "%H:%M:%S" 303 | #include the date if the date to be output happened before today, else just use the time 304 | if date.year==ti.year: 305 | if date.day==ti.day and date.month==ti.month: 306 | returnstring="" 307 | else: 308 | returnstring=date.strftime(f"{dateFormatString}, ") 309 | else: 310 | returnstring=date.strftime(f"{dateFormatString}, ") 311 | 312 | returnstring+=date.strftime(timeFormatString) 313 | except: 314 | pass 315 | return returnstring 316 | 317 | def isDuplicate(status,statuses): 318 | for i in statuses: 319 | if i.id==status.id: 320 | return True 321 | return False 322 | 323 | 324 | class dict_obj: 325 | def __init__(self, dict1): 326 | self.__dict__.update(dict1) 327 | 328 | def dict2obj(dict1): 329 | return json.loads(json.dumps(dict1), object_hook=dict_obj) 330 | 331 | def add_users(status): 332 | if status.user in globals.users: 333 | try: 334 | globals.users.remove(status.user) 335 | except: 336 | pass 337 | globals.users.insert(0,status.user) 338 | if hasattr(status,"quoted_status")!=False: 339 | if status.quoted_status.user in globals.users: 340 | try: 341 | globals.users.remove(status.quoted_status.user) 342 | except: 343 | pass 344 | globals.users.insert(0,status.quoted_status.user) 345 | if hasattr(status,"retweeted_status")!=False: 346 | if status.retweeted_status.user in globals.users: 347 | try: 348 | globals.users.remove(status.retweeted_status.user) 349 | except: 350 | pass 351 | globals.users.insert(0,status.retweeted_status.user) 352 | 353 | def lookup_user(id): 354 | for i in globals.users: 355 | if int(i.id)==int(id): 356 | return i 357 | globals.unknown_users.append(id) 358 | print(id+" not found. Added to cue of "+str(len(globals.unknown_users))+" users.") 359 | return None 360 | 361 | def lookup_user_name(account,name,use_api=True): 362 | for i in globals.users: 363 | if i.screen_name.lower()==name.lower(): 364 | return i 365 | if not use_api: 366 | return -1 367 | try: 368 | user=account.api.lookup_users(screen_name=[name])[0] 369 | if user in globals.users: 370 | try: 371 | globals.users.remove(user) 372 | except: 373 | pass 374 | globals.users.insert(0,user) 375 | return user 376 | except: 377 | return -1 378 | 379 | def get_user_objects_in_tweet(account,status,exclude_self=False,exclude_orig=False): 380 | users=[] 381 | if hasattr(status,"message_create"): 382 | users.append(lookup_user(status.message_create['sender_id'])) 383 | users.append(lookup_user(status.message_create['target']['recipient_id'])) 384 | return users 385 | if status.user not in users and not exclude_orig: 386 | users.append(status.user) 387 | if hasattr(status,"quoted_status")!=False and status.quoted_status.user not in users: 388 | users.append(status.quoted_status.user) 389 | if hasattr(status,"retweeted_status")!=False and status.retweeted_status.user not in users: 390 | users.append(status.retweeted_status.user) 391 | if hasattr(status,"entities") and "user_mentions" in status.entities: 392 | weew=status.entities['user_mentions'] 393 | for i in range(0,len(weew)): 394 | if (account.me.screen_name!=weew[i]['screen_name'] and exclude_self or not exclude_self): 395 | username=weew[i]['screen_name'] 396 | un=lookup_user_name(account,username) 397 | if un!=-1: 398 | users.append(un) 399 | for i in users: 400 | if i.id==account.me.id and exclude_self: 401 | users.remove(i) 402 | 403 | return users 404 | 405 | def speak_user(account,users): 406 | text="" 407 | for i in users: 408 | user=lookup_user_name(account,i) 409 | if user!=None and user!=-1: 410 | text+=". "+template_to_string(user,globals.prefs.userTemplate) 411 | text=text.rstrip(".") 412 | text=text.lstrip(".") 413 | speak.speak(str(len(users))+" users: "+text) 414 | 415 | def lookup_status(account,id): 416 | for i in account.timelines: 417 | for i2 in i.statuses: 418 | if i2.id==id: 419 | return i2 420 | s=account.api.get_status(id=id,tweet_mode="extended") 421 | return s 422 | 423 | def find_status(tl,id): 424 | index=0 425 | for i in tl.statuses: 426 | if i.id==id: 427 | return index 428 | index+=1 429 | 430 | return -1 431 | 432 | def find_reply(tl, id): 433 | index=0 434 | for i in tl.statuses: 435 | if hasattr(i,"in_reply_to_status_id") and i.in_reply_to_status_id==id: 436 | return index 437 | index+=1 438 | 439 | return -1 440 | 441 | def speak_reply(account,status): 442 | if hasattr(status,"in_reply_to_status_id") and status.in_reply_to_status_id!=None: 443 | status=lookup_status(account,status.in_reply_to_status_id) 444 | status=process_tweet(status) 445 | speak.speak(status) 446 | else: 447 | speak.speak("Not a reply.") 448 | 449 | def question(title,text, parent=None): 450 | dlg=wx.MessageDialog(parent,text,title,wx.YES_NO | wx.ICON_QUESTION) 451 | result=dlg.ShowModal() 452 | dlg.Destroy() 453 | if result== wx.ID_YES: 454 | return 1 455 | else: 456 | return 2 457 | 458 | def warn(message, caption = 'Warning!', parent=None): 459 | dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_WARNING) 460 | dlg.ShowModal() 461 | dlg.Destroy() 462 | 463 | def alert(message, caption = "", parent=None): 464 | dlg = wx.MessageDialog(parent, message, caption, wx.OK) 465 | dlg.ShowModal() 466 | dlg.Destroy() 467 | 468 | def cfu(silent=True): 469 | try: 470 | latest=json.loads(requests.get("https://api.github.com/repos/QuinterApp/Quinter/releases/latest",{"accept":"application/vnd.github.v3+json"}).content.decode()) 471 | if application.version