├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── forhire ├── libs │ ├── sql_helpers.py │ └── subreddits.py ├── main.py └── views │ ├── tab1.py │ ├── tab2.py │ └── tab3.py ├── requirements.txt └── screenshots ├── 1.png ├── 2.png └── 3.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: agentphantom 2 | patreon: agentphantom 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Phantom Insights 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # r/ForHire Helper 2 | 3 | r/ForHire Helper is a free and open source Python desktop application that helps freelancers and employers find the best candidates faster and easier. 4 | 5 | 6 | It uses the following libraries and technologies: 7 | 8 | * wxPython 4 for the GUI framework and to preview Reddit posts. 9 | * Requests to communicate with the Reddit JSON API. 10 | * SQLite to manage keywords, blacklists and bookmarks. 11 | 12 | ## Dependencies 13 | 14 | ``` 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | Note: macOS and Linux users must use `pip3` instead of `pip`. 19 | 20 | ## How to Use 21 | 22 | After you have downloaded this repository and installed the required dependencies you will only require to run the following commands. 23 | 24 | ``` 25 | cd path/to/forhire/ 26 | python main.py 27 | ``` 28 | 29 | Note: macOS and Linux users must use `python3` instead of `python`. 30 | 31 | After running those commands a new window will appear with 3 tabs. 32 | 33 | The first tab contains a button that will connect to the Reddit API and will download the latest posts from the selected Subreddit, the default one is [r/ForHire](https://www.reddit.com/r/forhire). 34 | 35 | Those posts will be filtered between `For Hire` and `Hiring`, or their equivalents in each subreddit. You can apply your custom keywords and blacklists by checking their respective boxes. 36 | 37 | Adding new keywords and blacklists is done in the second tab. Where you would only require to type individual words and press the `Add` button. Selecting a previous saved item and pressing the `Delete` button will delete it from your database. 38 | 39 | In the third tab you will be presented with all the posts you have bookmarked from the first tab. Everytime you navigate to this tab the bookmarks will be automatically updated. 40 | 41 | ## Preview 42 | 43 | ![Main View](screenshots/1.png) 44 | 45 | ## Download 46 | 47 | For your convenience a Windows 64-bit executable is available, you can download it directly from GitHub. 48 | 49 | [![Download](https://i.imgur.com/QEtsNHg.png)](https://github.com/PhantomInsights/forhirehelper/releases/download/v0.3/forhire.exe) 50 | 51 | ## Become a Patron 52 | 53 | Feel free to support the development of free tools and software. Your donations are greatly appreciated. 54 | 55 | [![Become a Patron!](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/bePatron?u=20521425) 56 | -------------------------------------------------------------------------------- /forhire/libs/sql_helpers.py: -------------------------------------------------------------------------------- 1 | """DB Helpers that abstract SQL queries.""" 2 | 3 | import sqlite3 4 | 5 | 6 | def init_db(): 7 | """Creates a database if it doesn't exist.""" 8 | 9 | conn = sqlite3.connect("data.db") 10 | cursor = conn.cursor() 11 | create_tables(cursor) 12 | return conn 13 | 14 | 15 | def create_tables(cursor): 16 | """Creates the tables.""" 17 | 18 | keywords_query = """CREATE TABLE IF NOT EXISTS keywords (word TEXT UNIQUE)""" 19 | blacklist_query = """CREATE TABLE IF NOT EXISTS blacklist (word TEXT UNIQUE)""" 20 | posts_query = """CREATE TABLE IF NOT EXISTS posts (post_id TEXT UNIQUE, subreddit TEXT, 21 | flair TEXT, author TEXT, title TEXT, link TEXT, selftext TEXT, pub_date TEXT) """ 22 | 23 | cursor.execute(keywords_query) 24 | cursor.execute(blacklist_query) 25 | cursor.execute(posts_query) 26 | 27 | 28 | def insert_word_to_table(conn, table_name, value): 29 | """Inserts a word to the specified table.""" 30 | 31 | with conn: 32 | query = "INSERT INTO {} VALUES (?)".format(table_name) 33 | conn.execute(query, (value,)) 34 | 35 | 36 | def delete_word_from_table(conn, table_name, value): 37 | """Deletes a word from the specified table.""" 38 | 39 | with conn: 40 | query = "DELETE FROM {} WHERE word=?".format(table_name) 41 | conn.execute(query, (value,)) 42 | 43 | 44 | def load_words(conn, table_name): 45 | """Returns the values from the specified table.""" 46 | 47 | with conn: 48 | query = "SELECT * FROM {} ORDER BY word ASC".format(table_name) 49 | return conn.execute(query) 50 | 51 | 52 | def insert_post_to_table(conn, data_dict): 53 | """Inserts a post to the posts table.""" 54 | 55 | with conn: 56 | query = "INSERT INTO posts VALUES (?, ?, ?, ?, ?, ?, ?, ?)" 57 | conn.execute(query, (data_dict["post_id"], data_dict["subreddit"], data_dict["flair"], 58 | data_dict["author"], data_dict["title"], data_dict["link"], 59 | data_dict["text"], data_dict["pub_date"])) 60 | 61 | 62 | def delete_post_from_table(conn, value): 63 | """Deletes a post from the posts table.""" 64 | 65 | with conn: 66 | query = "DELETE FROM posts WHERE post_id=?" 67 | conn.execute(query, (value,)) 68 | 69 | 70 | def load_posts(conn, value): 71 | """Returns all the posts from the posts table.""" 72 | 73 | with conn: 74 | query = "SELECT * FROM posts WHERE subreddit='{}'".format(value) 75 | return conn.execute(query) 76 | -------------------------------------------------------------------------------- /forhire/libs/subreddits.py: -------------------------------------------------------------------------------- 1 | """ 2 | This list contains the subreddits used in this program. 3 | The rules key is used to filter the posts between those who seek jobs and those who offer them. 4 | """ 5 | 6 | SUBREDDITS_LIST = [ 7 | {"id": "forhire", "name": "r/ForHire", "rules": ["For Hire", "Hiring"]}, 8 | {"id": "designjobs", "name": "r/DesignJobs", "rules": ["For Hire", "Hiring"]}, 9 | {"id": "hireawriter", "name": "r/HireaWriter", "rules": ["Hire Me", "Hiring"]}, 10 | {"id": "jobbit", "name": "r/jobbit", "rules": ["For Hire", "Hiring"]}, 11 | {"id": "jobs4bitcoins", "name": "r/Jobs4Bitcoins", "rules": ["For Hire", "Hiring"]}, 12 | {"id": "jobs4crypto", "name": "r/Jobs4Crypto", "rules": ["LFW", "Job"]}, 13 | {"id": "slavelabour", "name": "r/SlaveLabour", "rules": ["Offer", "Task"]} 14 | ] -------------------------------------------------------------------------------- /forhire/main.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | from libs import sql_helpers 4 | from views import tab1, tab2, tab3 5 | 6 | sql_conn = sql_helpers.init_db() 7 | 8 | 9 | class Root(wx.Frame): 10 | 11 | def __init__(self, parent): 12 | 13 | wx.Frame.__init__( 14 | self, parent, title="r/ForHire Helper v0.3", size=(1000, 800)) 15 | 16 | self.notebook = wx.Notebook(self) 17 | 18 | self.page1 = tab1.Tab1(self.notebook) 19 | self.page2 = tab2.Tab2(self.notebook) 20 | self.page3 = tab3.Tab3(self.notebook) 21 | 22 | self.notebook.AddPage(self.page1, "Posts Explorer") 23 | self.notebook.AddPage(self.page2, text="Keywords and Blacklist") 24 | self.notebook.AddPage(self.page3, text="Bookmarks") 25 | 26 | self.Show(True) 27 | 28 | 29 | if __name__ == "__main__": 30 | app = wx.App(False) 31 | frame = Root(None) 32 | frame.Center() 33 | app.MainLoop() 34 | -------------------------------------------------------------------------------- /forhire/views/tab1.py: -------------------------------------------------------------------------------- 1 | """ 2 | This tab displays the latest r/ForHire posts in a table. 3 | Filters are also applied using the values from the keywords and blacklist tables. 4 | A preview of the post is shown at the bottom. 5 | It also has some extra utilities to open Reddit url's. 6 | """ 7 | 8 | import html 9 | import time 10 | import webbrowser 11 | from datetime import datetime 12 | 13 | import requests 14 | import wx 15 | import wx.html2 16 | from wx.adv import NotificationMessage 17 | 18 | import main 19 | from libs import sql_helpers 20 | from libs.subreddits import SUBREDDITS_LIST 21 | 22 | 23 | class Tab1(wx.Panel): 24 | 25 | def __init__(self, *args): 26 | wx.Panel.__init__(self, *args) 27 | 28 | self.selected_post = "" 29 | self.posts_list = list() 30 | self.m_keywords = list() 31 | self.blacklist = list() 32 | 33 | self.main_sizer = wx.BoxSizer(wx.VERTICAL) 34 | 35 | self.title_label = wx.StaticText( 36 | self, label="Configure your search parameters.") 37 | 38 | self.main_sizer.Add( 39 | self.title_label, wx.SizerFlags().Center().Border(wx.ALL, 5)) 40 | 41 | self.top_sizer = wx.BoxSizer(wx.HORIZONTAL) 42 | self.main_sizer.Add(self.top_sizer, wx.SizerFlags( 43 | ).Centre().Center().Border(wx.ALL, 5)) 44 | 45 | self.top_sizer_flags = wx.SizerFlags( 46 | 1).Center().Expand().Border(wx.ALL, 5) 47 | 48 | self.subreddit_label = wx.StaticText(self, label="Select a Subreddit:") 49 | self.top_sizer.Add(self.subreddit_label, wx.SizerFlags().Centre()) 50 | 51 | self.subreddit_type = wx.ComboBox( 52 | self, style=wx.CB_READONLY, choices=[item["name"] for item in SUBREDDITS_LIST]) 53 | self.Bind(wx.EVT_COMBOBOX, self.select_subreddit, self.subreddit_type) 54 | self.top_sizer.Add(self.subreddit_type, self.top_sizer_flags) 55 | 56 | self.post_type_label = wx.StaticText(self, label="Select a Post type:") 57 | self.top_sizer.Add(self.post_type_label, wx.SizerFlags().Centre()) 58 | 59 | self.post_type = wx.ComboBox(self, style=wx.CB_READONLY) 60 | self.Bind(wx.EVT_COMBOBOX, self.filter_results, self.post_type) 61 | self.top_sizer.Add(self.post_type, self.top_sizer_flags) 62 | 63 | self.m_keywords_checkbox = wx.CheckBox(self, label="Apply Keywords") 64 | self.Bind(wx.EVT_CHECKBOX, self.filter_results, 65 | self.m_keywords_checkbox) 66 | self.top_sizer.Add(self.m_keywords_checkbox, self.top_sizer_flags) 67 | 68 | self.blacklist_checkbox = wx.CheckBox( 69 | self, label="Apply Blacklist") 70 | self.Bind(wx.EVT_CHECKBOX, self.filter_results, 71 | self.blacklist_checkbox) 72 | self.top_sizer.Add( 73 | self.blacklist_checkbox, self.top_sizer_flags) 74 | 75 | self.search_button = wx.Button(self, label="Search") 76 | self.Bind(wx.EVT_BUTTON, self.do_search, self.search_button) 77 | self.top_sizer.Add(self.search_button, self.top_sizer_flags) 78 | 79 | self.posts_table = wx.ListCtrl( 80 | self, style=wx.LC_REPORT | wx.BORDER_SUNKEN) 81 | self.posts_table.InsertColumn(0, "Post ID", width=100) 82 | self.posts_table.InsertColumn(1, "Published Date", width=150) 83 | self.posts_table.InsertColumn(2, "Author", width=180) 84 | self.posts_table.InsertColumn(3, "Title", width=-1) 85 | self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.selected_item) 86 | self.main_sizer.Add(self.posts_table, wx.SizerFlags( 87 | 1).Expand().Border(wx.ALL, 5)) 88 | 89 | self.mid_sizer = wx.BoxSizer(wx.HORIZONTAL) 90 | self.main_sizer.Add(self.mid_sizer, wx.SizerFlags( 91 | ).Centre().Center().Border(wx.ALL, 5)) 92 | 93 | self.dm_button = wx.Button(self, label="Send Message") 94 | self.Bind(wx.EVT_BUTTON, self.send_dm, self.dm_button) 95 | self.mid_sizer.Add(self.dm_button, self.top_sizer_flags) 96 | 97 | self.link_button = wx.Button(self, label="View on Reddit") 98 | self.Bind(wx.EVT_BUTTON, self.open_reddit, self.link_button) 99 | self.mid_sizer.Add(self.link_button, self.top_sizer_flags) 100 | 101 | self.save_button = wx.Button(self, label="Add to Bookmarks") 102 | self.Bind(wx.EVT_BUTTON, self.add_to_bookmarks, self.save_button) 103 | self.mid_sizer.Add(self.save_button, self.top_sizer_flags) 104 | 105 | self.html_content = wx.html2.WebView.New(self, size=(-1, 400)) 106 | self.main_sizer.Add(self.html_content, wx.SizerFlags( 107 | 1).Expand().Border(wx.ALL, 5)) 108 | 109 | self.SetSizer(self.main_sizer) 110 | self.Bind(wx.EVT_SHOW, self.show_handler) 111 | 112 | # We initialize the tab with the first subreeddit. 113 | self.subreddit_type.SetValue(SUBREDDITS_LIST[0]["name"]) 114 | self.select_subreddit(None) 115 | 116 | def show_handler(self, event): 117 | """Called when the Panel is being shown.""" 118 | 119 | if event.IsShown(): 120 | self.m_keywords = [item[0].lower() for item in sql_helpers.load_words( 121 | main.sql_conn, "keywords")] 122 | 123 | self.blacklist = [item[0].lower() for item in sql_helpers.load_words( 124 | main.sql_conn, "blacklist")] 125 | 126 | def select_subreddit(self, event): 127 | """Populates the post type ComboBox with the data from the selected subreddit.""" 128 | 129 | for subreddit in SUBREDDITS_LIST: 130 | if subreddit["name"] == self.subreddit_type.Value: 131 | rules = subreddit["rules"] 132 | break 133 | 134 | self.post_type.Clear() 135 | self.post_type.AppendItems(rules) 136 | self.post_type.SetValue(rules[0]) 137 | 138 | def do_search(self, event): 139 | """Starts the search.""" 140 | 141 | self.posts_list = list() 142 | self.load_posts() 143 | self.filter_results(None) 144 | 145 | def filter_results(self, event): 146 | """ 147 | Applies the filters to the results. 148 | The way it works is a bit hacky. 149 | 150 | Blacklist items takes precedence over keywords. 151 | 152 | When an item checks against the blacklist it is removed 153 | from the main list. 154 | 155 | When an item checks against the keywords, it is added 156 | to a new list. And this new list is used to feed the table. 157 | """ 158 | 159 | self.posts_table.DeleteAllItems() 160 | temp_posts = self.posts_list[:] 161 | filtered_posts = list() 162 | 163 | if self.blacklist_checkbox.IsChecked(): 164 | for item in reversed(temp_posts): 165 | if self.quick_filter(self.blacklist, item): 166 | temp_posts.remove(item) 167 | 168 | if self.m_keywords_checkbox.IsChecked(): 169 | for item in temp_posts: 170 | if self.quick_filter(self.m_keywords, item): 171 | filtered_posts.append(item) 172 | 173 | if len(filtered_posts) == 0 and self.m_keywords_checkbox.IsChecked(): 174 | # No results, we do nothing. 175 | pass 176 | elif len(filtered_posts) >= 1: 177 | for item in filtered_posts: 178 | if self.post_type.GetValue().lower() in item["flair"].lower(): 179 | self.posts_table.Append( 180 | [item["post_id"], item["pub_date"], item["author"], item["title"]]) 181 | else: 182 | for item in temp_posts: 183 | if self.post_type.GetValue().lower() in item["flair"].lower(): 184 | self.posts_table.Append( 185 | [item["post_id"], item["pub_date"], item["author"], item["title"]]) 186 | 187 | self.posts_table.SetColumnWidth(3, -1) 188 | 189 | def quick_filter(self, words, item): 190 | """Applies a quick filter iterating over a list of values.""" 191 | 192 | for word in words: 193 | if word in item["title"].lower() or word in item["text"].lower(): 194 | return True 195 | return False 196 | 197 | def load_posts(self, after="", counter=0, target=200): 198 | """ 199 | Loads the latest posts from the selected subreddit. 200 | By default it downloads the latest 200. 201 | this cnn be changed in the target default value, the max is 1000. 202 | """ 203 | 204 | for subreddit in SUBREDDITS_LIST: 205 | if subreddit["name"] == self.subreddit_type.Value: 206 | selected_subreddit = subreddit["id"] 207 | break 208 | 209 | headers = {"User-Agent": "r/ForHire Helper v0.3"} 210 | 211 | if after == "": 212 | url = "https://www.reddit.com/r/{}/new/.json?limit=100".format( 213 | selected_subreddit) 214 | else: 215 | url = "https://www.reddit.com/r/{}/new/.json?limit=100&after={}".format( 216 | selected_subreddit, after) 217 | 218 | with requests.get(url, headers=headers) as response: 219 | 220 | if response.status_code == 200: 221 | counter += 100 222 | 223 | for submission in response.json()["data"]["children"]: 224 | 225 | # Very rarely a post has no text. 226 | # We set it to an empty string instead of a null value. 227 | if submission["data"]["selftext_html"] is None: 228 | submission_text = "" 229 | else: 230 | submission_text = submission["data"]["selftext_html"] 231 | 232 | # We detect if a post has not been flaired. 233 | # If not, we fallback to using its title. 234 | if submission["data"]["link_flair_text"] is None: 235 | flair = submission["data"]["title"] 236 | else: 237 | flair = submission["data"]["link_flair_text"] 238 | 239 | post_dict = { 240 | "post_id": submission["data"]["id"], 241 | "flair": flair, 242 | "subreddit": submission["data"]["subreddit"].lower(), 243 | "author": submission["data"]["author"], 244 | "title": submission["data"]["title"], 245 | "link": submission["data"]["url"], 246 | "text": submission_text, 247 | 248 | "pub_date": datetime.fromtimestamp( 249 | submission["data"]["created"]).strftime("%Y-%m-%d @ %H:%M:%S")} 250 | 251 | self.posts_list.append(post_dict) 252 | 253 | if counter >= target: 254 | pass 255 | else: 256 | time.sleep(1.2) 257 | self.load_posts(response.json()["data"]["after"], counter) 258 | 259 | def selected_item(self, event): 260 | """When an item is selected show the text in the WebView.""" 261 | 262 | self.selected_post = event.GetText() 263 | 264 | for item in self.posts_list: 265 | if self.selected_post == item["post_id"]: 266 | self.html_content.SetPage(html.unescape(item["text"]), "") 267 | break 268 | 269 | def send_dm(self, event): 270 | """opens the default web browser with prefilled subject and title.""" 271 | 272 | base_url = "https://www.reddit.com/message/compose/?to={}&subject=RE: {}" 273 | 274 | for item in self.posts_list: 275 | if self.selected_post == item["post_id"]: 276 | webbrowser.open(base_url.format(item["author"], item["title"])) 277 | break 278 | 279 | def open_reddit(self, event): 280 | """opens the default web browser with the post url.""" 281 | 282 | base_url = "https://redd.it/{}" 283 | 284 | for item in self.posts_list: 285 | if self.selected_post == item["post_id"]: 286 | webbrowser.open(base_url.format(item["post_id"])) 287 | break 288 | 289 | def add_to_bookmarks(self, event): 290 | """Adds the selected post to the bookmarks table.""" 291 | 292 | for item in self.posts_list: 293 | if self.selected_post == item["post_id"]: 294 | sql_helpers.insert_post_to_table(main.sql_conn, item) 295 | temp_toast = NotificationMessage( 296 | title="Success", message="Post successfully saved.") 297 | temp_toast.Show() 298 | break 299 | -------------------------------------------------------------------------------- /forhire/views/tab2.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this tab you can manage your keywords and blacklist items. 3 | 4 | The code in this tab is highly reusable since both keywords and blacklists are 5 | based on the same principle. 6 | """ 7 | 8 | import wx 9 | 10 | import main 11 | from libs import sql_helpers 12 | 13 | 14 | class Tab2(wx.Panel): 15 | 16 | def __init__(self, *args): 17 | wx.Panel.__init__(self, *args) 18 | 19 | self.main_sizer = wx.BoxSizer(wx.HORIZONTAL) 20 | 21 | self.left_sizer = wx.BoxSizer(wx.VERTICAL) 22 | self.right_sizer = wx.BoxSizer(wx.VERTICAL) 23 | 24 | self.main_sizer.Add(self.left_sizer, wx.SizerFlags( 25 | 1).Centre().Center().Expand().Border(wx.ALL, 5)) 26 | self.main_sizer.Add(self.right_sizer, wx.SizerFlags( 27 | 1).Centre().Center().Expand().Border(wx.ALL, 5)) 28 | 29 | ######## 30 | # Keywords section starts here. 31 | ######## 32 | 33 | self.keywords_label = wx.StaticText(self, label="Keywords") 34 | self.left_sizer.Add(self.keywords_label, 35 | wx.SizerFlags().Center().Border(wx.ALL, 5)) 36 | 37 | self.keywords_desc = wx.StaticText( 38 | self, label="Add keywords that you are looking for, e.g: python, nodejs, seo, writing") 39 | self.left_sizer.Add(self.keywords_desc, 40 | wx.SizerFlags().Expand().Left().Border(wx.ALL, 5)) 41 | 42 | self.left_sub_sizer = wx.BoxSizer(wx.HORIZONTAL) 43 | self.left_sizer.Add(self.left_sub_sizer, wx.SizerFlags( 44 | ).Expand().Centre().Border(wx.ALL, 5)) 45 | 46 | self.keywords_entry = wx.TextCtrl(self) 47 | self.left_sub_sizer.Add(self.keywords_entry, wx.SizerFlags( 48 | 1).Expand().Centre().Border(wx.ALL, 5)) 49 | 50 | self.keywords_add_button = wx.Button(self, label="Add") 51 | self.Bind(wx.EVT_BUTTON, lambda event, table_name="keywords": self.add_word( 52 | event, table_name), self.keywords_add_button) 53 | self.left_sub_sizer.Add(self.keywords_add_button, 54 | wx.SizerFlags().Centre().Border(wx.ALL, 5)) 55 | 56 | self.keywords_del_button = wx.Button(self, label="Delete") 57 | self.Bind(wx.EVT_BUTTON, lambda event, table_name="keywords": self.delete_word( 58 | event, table_name), self.keywords_del_button) 59 | self.left_sub_sizer.Add(self.keywords_del_button, 60 | wx.SizerFlags().Centre().Border(wx.ALL, 5)) 61 | 62 | self.keywords_list = wx.ListBox(self, style=wx.LB_SINGLE) 63 | self.left_sizer.Add(self.keywords_list, wx.SizerFlags( 64 | 1).Expand().Center().Border(wx.ALL, 5)) 65 | 66 | ######## 67 | # Blacklist section starts here. 68 | ######## 69 | 70 | self.blacklist_label = wx.StaticText(self, label="Blacklist") 71 | self.right_sizer.Add(self.blacklist_label, 72 | wx.SizerFlags().Center().Border(wx.ALL, 5)) 73 | 74 | self.blacklist_desc = wx.StaticText( 75 | self, label="Add words that you want to filter out, e.g: simple, rockstar, ninja, crypto") 76 | self.right_sizer.Add(self.blacklist_desc, 77 | wx.SizerFlags().Expand().Left().Border(wx.ALL, 5)) 78 | 79 | self.right_sub_sizer = wx.BoxSizer(wx.HORIZONTAL) 80 | self.right_sizer.Add(self.right_sub_sizer, 81 | wx.SizerFlags().Expand().Centre().Border(wx.ALL, 5)) 82 | 83 | self.blacklist_entry = wx.TextCtrl(self) 84 | self.right_sub_sizer.Add( 85 | self.blacklist_entry, wx.SizerFlags(1).Expand().Centre().Border(wx.ALL, 5)) 86 | 87 | self.blacklist_add_button = wx.Button(self, label="Add") 88 | self.Bind(wx.EVT_BUTTON, lambda event, table_name="blacklist": self.add_word( 89 | event, table_name), self.blacklist_add_button) 90 | self.right_sub_sizer.Add( 91 | self.blacklist_add_button, wx.SizerFlags().Centre().Border(wx.ALL, 5)) 92 | 93 | self.blacklist_del_button = wx.Button(self, label="Delete") 94 | self.Bind(wx.EVT_BUTTON, lambda event, table_name="blacklist": self.delete_word( 95 | event, table_name), self.blacklist_del_button) 96 | self.right_sub_sizer.Add( 97 | self.blacklist_del_button, wx.SizerFlags().Centre().Border(wx.ALL, 5)) 98 | 99 | self.blacklist_list = wx.ListBox(self, style=wx.LB_SINGLE) 100 | self.right_sizer.Add(self.blacklist_list, wx.SizerFlags( 101 | 1).Expand().Center().Border(wx.ALL, 5)) 102 | 103 | self.SetSizer(self.main_sizer) 104 | self.Bind(wx.EVT_SHOW, self.show_handler) 105 | 106 | def show_handler(self, event): 107 | """Called when the Panel is being shown.""" 108 | 109 | if event.IsShown(): 110 | self.load_words("keywords") 111 | self.load_words("blacklist") 112 | 113 | def add_word(self, event, table_name): 114 | """Adds the new word to the database.""" 115 | 116 | if table_name == "keywords": 117 | new_word = self.keywords_entry.Value 118 | elif table_name == "blacklist": 119 | new_word = self.blacklist_entry.Value 120 | 121 | if new_word != "": 122 | sql_helpers.insert_word_to_table( 123 | main.sql_conn, table_name, new_word) 124 | self.load_words(table_name) 125 | 126 | if table_name == "keywords": 127 | self.keywords_entry.SetValue("") 128 | elif table_name == "blacklist": 129 | self.blacklist_entry.SetValue("") 130 | 131 | def delete_word(self, event, table_name): 132 | """Deletes the selected word from the database.""" 133 | 134 | if table_name == "keywords": 135 | selected_word = self.keywords_list.StringSelection 136 | elif table_name == "blacklist": 137 | selected_word = self.blacklist_list.StringSelection 138 | 139 | if selected_word != "": 140 | sql_helpers.delete_word_from_table( 141 | main.sql_conn, table_name, selected_word) 142 | self.load_words(table_name) 143 | 144 | def load_words(self, table_name): 145 | """Loads the values from keywords or blacklist.""" 146 | 147 | temp_words = sql_helpers.load_words(main.sql_conn, table_name) 148 | 149 | if table_name == "keywords": 150 | self.keywords_list.Clear() 151 | 152 | for word in temp_words: 153 | self.keywords_list.Append(word[0]) 154 | 155 | elif table_name == "blacklist": 156 | self.blacklist_list.Clear() 157 | 158 | for word in temp_words: 159 | self.blacklist_list.Append(word[0]) 160 | -------------------------------------------------------------------------------- /forhire/views/tab3.py: -------------------------------------------------------------------------------- 1 | """ 2 | This tab displays all the posts the user has bookmarked. 3 | 4 | It also shares some features from the first tab. The biggest 5 | differences are the Search button is not present and 6 | there's a Delete button instead of an Add button. 7 | """ 8 | 9 | import html 10 | import webbrowser 11 | from datetime import datetime 12 | 13 | import wx 14 | import wx.html2 15 | from wx.adv import NotificationMessage 16 | 17 | import main 18 | from libs import sql_helpers 19 | from libs.subreddits import SUBREDDITS_LIST 20 | 21 | 22 | class Tab3(wx.Panel): 23 | 24 | def __init__(self, *args): 25 | wx.Panel.__init__(self, *args) 26 | 27 | self.selected_post = "" 28 | self.posts_list = list() 29 | self.keywords = list() 30 | self.blacklist = list() 31 | 32 | self.main_sizer = wx.BoxSizer(wx.VERTICAL) 33 | 34 | self.title_label = wx.StaticText( 35 | self, label="Manage and filter your bookmarks.") 36 | 37 | self.main_sizer.Add( 38 | self.title_label, wx.SizerFlags().Center().Border(wx.ALL, 5)) 39 | 40 | self.top_sizer = wx.BoxSizer(wx.HORIZONTAL) 41 | self.main_sizer.Add(self.top_sizer, wx.SizerFlags( 42 | ).Centre().Center().Border(wx.ALL, 5)) 43 | 44 | self.top_sizer_flags = wx.SizerFlags( 45 | 1).Center().Expand().Border(wx.ALL, 5) 46 | 47 | self.subreddit_label = wx.StaticText(self, label="Select a Subreddit:") 48 | self.top_sizer.Add(self.subreddit_label, wx.SizerFlags().Centre()) 49 | 50 | self.subreddit_type = wx.ComboBox( 51 | self, style=wx.CB_READONLY, choices=[item["name"] for item in SUBREDDITS_LIST]) 52 | self.Bind(wx.EVT_COMBOBOX, self.select_subreddit, self.subreddit_type) 53 | self.top_sizer.Add(self.subreddit_type, self.top_sizer_flags) 54 | 55 | self.post_type_label = wx.StaticText(self, label="Select a Post type:") 56 | self.top_sizer.Add(self.post_type_label, wx.SizerFlags().Centre()) 57 | 58 | self.post_type = wx.ComboBox(self, style=wx.CB_READONLY) 59 | self.Bind(wx.EVT_COMBOBOX, self.filter_results, self.post_type) 60 | self.top_sizer.Add(self.post_type, self.top_sizer_flags) 61 | 62 | self.keywords_checkbox = wx.CheckBox(self, label="Apply Keywords") 63 | self.Bind(wx.EVT_CHECKBOX, self.filter_results, self.keywords_checkbox) 64 | self.top_sizer.Add(self.keywords_checkbox, self.top_sizer_flags) 65 | 66 | self.blacklist_checkbox = wx.CheckBox( 67 | self, label="Apply Blacklist") 68 | self.Bind(wx.EVT_CHECKBOX, self.filter_results, 69 | self.blacklist_checkbox) 70 | self.top_sizer.Add( 71 | self.blacklist_checkbox, self.top_sizer_flags) 72 | 73 | self.posts_table = wx.ListCtrl( 74 | self, style=wx.LC_REPORT | wx.BORDER_SUNKEN) 75 | self.posts_table.InsertColumn(0, "Post ID", width=100) 76 | self.posts_table.InsertColumn(1, "Published Date", width=150) 77 | self.posts_table.InsertColumn(2, "Author", width=180) 78 | self.posts_table.InsertColumn(3, "Title", width=-1) 79 | self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.selected_item) 80 | self.main_sizer.Add(self.posts_table, wx.SizerFlags( 81 | 1).Expand().Border(wx.ALL, 5)) 82 | 83 | self.mid_sizer = wx.BoxSizer(wx.HORIZONTAL) 84 | self.main_sizer.Add(self.mid_sizer, wx.SizerFlags( 85 | ).Centre().Center().Border(wx.ALL, 5)) 86 | 87 | self.dm_button = wx.Button(self, label="Send Message") 88 | self.Bind(wx.EVT_BUTTON, self.send_dm, self.dm_button) 89 | self.mid_sizer.Add(self.dm_button, self.top_sizer_flags) 90 | 91 | self.link_button = wx.Button(self, label="View on Reddit") 92 | self.Bind(wx.EVT_BUTTON, self.open_reddit, self.link_button) 93 | self.mid_sizer.Add(self.link_button, self.top_sizer_flags) 94 | 95 | self.del_button = wx.Button(self, label="Delete from Bookmarks") 96 | self.Bind(wx.EVT_BUTTON, self.delete_from_bookmarks, self.del_button) 97 | self.mid_sizer.Add(self.del_button, self.top_sizer_flags) 98 | 99 | self.html_content = wx.html2.WebView.New(self, size=(-1, 400)) 100 | self.main_sizer.Add(self.html_content, wx.SizerFlags( 101 | 1).Expand().Border(wx.ALL, 5)) 102 | 103 | self.SetSizer(self.main_sizer) 104 | self.Bind(wx.EVT_SHOW, self.show_handler) 105 | 106 | # We initialize the tab with the first subreeddit. 107 | self.subreddit_type.SetValue(SUBREDDITS_LIST[0]["name"]) 108 | self.select_subreddit(None) 109 | 110 | def show_handler(self, event): 111 | """Called when the Panel is being shown.""" 112 | 113 | if event.IsShown(): 114 | self.keywords = [item[0].lower() for item in sql_helpers.load_words( 115 | main.sql_conn, "keywords")] 116 | 117 | self.blacklist = [item[0].lower() for item in sql_helpers.load_words( 118 | main.sql_conn, "blacklist")] 119 | 120 | self.load_posts() 121 | 122 | def select_subreddit(self, event): 123 | """Populates the post type ComboBox with the data from the selected subreddit.""" 124 | 125 | for subreddit in SUBREDDITS_LIST: 126 | if subreddit["name"] == self.subreddit_type.Value: 127 | rules = subreddit["rules"] 128 | break 129 | 130 | self.post_type.Clear() 131 | self.post_type.AppendItems(rules) 132 | self.post_type.SetValue(rules[0]) 133 | self.load_posts() 134 | 135 | def filter_results(self, event): 136 | """ 137 | Applies the filters to the results. 138 | The way it works is a bit hacky. 139 | 140 | Blacklist items takes precedence over keywords. 141 | 142 | When an item checks against the blacklist it is removed 143 | from the main list. 144 | 145 | When an item checks against the keywords, it is added 146 | to a new list. And this new list is used to feed the table. 147 | """ 148 | 149 | self.posts_table.DeleteAllItems() 150 | temp_posts = self.posts_list[:] 151 | filtered_posts = list() 152 | 153 | if self.blacklist_checkbox.IsChecked(): 154 | for item in reversed(temp_posts): 155 | if self.quick_filter(self.blacklist, item): 156 | temp_posts.remove(item) 157 | 158 | if self.keywords_checkbox.IsChecked(): 159 | for item in temp_posts: 160 | if self.quick_filter(self.keywords, item): 161 | filtered_posts.append(item) 162 | 163 | if len(filtered_posts) == 0 and self.keywords_checkbox.IsChecked(): 164 | # No results, we do nothing. 165 | pass 166 | elif len(filtered_posts) >= 1: 167 | for item in filtered_posts: 168 | if self.post_type.GetValue().lower() in item["flair"].lower(): 169 | self.posts_table.Append( 170 | [item["post_id"], item["pub_date"], item["author"], item["title"]]) 171 | else: 172 | for item in temp_posts: 173 | if self.post_type.GetValue().lower() in item["flair"].lower(): 174 | self.posts_table.Append( 175 | [item["post_id"], item["pub_date"], item["author"], item["title"]]) 176 | 177 | self.posts_table.SetColumnWidth(3, -1) 178 | 179 | def quick_filter(self, words, item): 180 | """Applies a quick filter iterating over a list of values.""" 181 | 182 | for word in words: 183 | if word in item["title"].lower() or word in item["text"].lower(): 184 | return True 185 | return False 186 | 187 | def load_posts(self): 188 | """Loads posts from the posts table specifying a subreddit.""" 189 | 190 | for subreddit in SUBREDDITS_LIST: 191 | if subreddit["name"] == self.subreddit_type.Value: 192 | selected_subreddit = subreddit["id"] 193 | break 194 | 195 | self.posts_list = list() 196 | 197 | for item in sql_helpers.load_posts(main.sql_conn, selected_subreddit): 198 | self.posts_list.append( 199 | {"post_id": item[0], "subreddit": item[1], "flair": item[2], "author": item[3], "title": item[4], 200 | "link": item[5], "text": item[6], "pub_date": item[7]}) 201 | 202 | self.filter_results(None) 203 | 204 | def selected_item(self, event): 205 | """When an item is selected show the text in the WebView.""" 206 | 207 | self.selected_post = event.GetText() 208 | 209 | for item in self.posts_list: 210 | if self.selected_post == item["post_id"]: 211 | self.html_content.SetPage(html.unescape(item["text"]), "") 212 | break 213 | 214 | def send_dm(self, event): 215 | """opens the default web browser with prefilled subject and title.""" 216 | 217 | base_url = "https://www.reddit.com/message/compose/?to={}&subject=RE: {}" 218 | 219 | for item in self.posts_list: 220 | if self.selected_post == item["post_id"]: 221 | webbrowser.open(base_url.format(item["author"], item["title"])) 222 | break 223 | 224 | def open_reddit(self, event): 225 | """opens the default web browser with the post url.""" 226 | 227 | base_url = "https://redd.it/{}" 228 | 229 | for item in self.posts_list: 230 | if self.selected_post == item["post_id"]: 231 | webbrowser.open(base_url.format(item["post_id"])) 232 | break 233 | 234 | def delete_from_bookmarks(self, event): 235 | """Delete the selected post from the bookmarks table.""" 236 | 237 | for item in self.posts_list: 238 | if self.selected_post == item["post_id"]: 239 | sql_helpers.delete_post_from_table( 240 | main.sql_conn, item["post_id"]) 241 | temp_toast = NotificationMessage( 242 | title="Success", message="Post successfully deleted.") 243 | temp_toast.Show() 244 | self.load_posts() 245 | self.html_content.SetPage("", "") 246 | break 247 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhantomInsights/forhirehelper/fc8066e78d1ea7469879b8d36b3f4a26c344a93e/requirements.txt -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhantomInsights/forhirehelper/fc8066e78d1ea7469879b8d36b3f4a26c344a93e/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhantomInsights/forhirehelper/fc8066e78d1ea7469879b8d36b3f4a26c344a93e/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhantomInsights/forhirehelper/fc8066e78d1ea7469879b8d36b3f4a26c344a93e/screenshots/3.png --------------------------------------------------------------------------------