├── requirements.txt ├── README.md ├── LICENSE └── ti.py /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.4.1 2 | decorator==4.0.9 3 | praw==3.5.0 4 | praw-oauth2util==0.3.4 5 | requests==2.10.0 6 | six==1.10.0 7 | update-checker==0.11 8 | psycopg2==2.6.1 9 | urlparse2==1.1.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TweakInfoBot 2 | 3 | [/u/TweakInfoBot](https://www.reddit.com/user/TweakInfoBot) on Reddit 4 | 5 | Inspiration for this bot came from [/u/hearthscan-bot](https://www.reddit.com/user/hearthscan-bot) and [/u/Healdb](https://www.reddit.com/user/healdb) who made [/r/tweakinfo](https://www.reddit.com/r/tweakinfo/). 6 | 7 | This Reddit bot runs on /r/jailbreak and /r/iOSthemes and looks for tweak and theme queries and returns information about those things. 8 | 9 | Hosted on Heroku, major decisions in how this bot works are with the intention of having the bot run at no cost to me. 10 | 11 | ### Features 12 | 13 | 1. *bleep boop* 14 | 2. Checks posts and comments 15 | 3. Returns direct Cydia link, repo, cost, type, and short description 16 | 17 | ### Planned Features 18 | 19 | 1. Reply to PMs 20 | 2. Work with non-default repositories 21 | 3. Better tweak name recognition (account for spelling errors, etc) 22 | 23 | ### License 24 | 25 | [See LICENSE](https://github.com/hizinfiz/TweakInfoBot/blob/master/LICENSE) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 hizinfiz 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 | -------------------------------------------------------------------------------- /ti.py: -------------------------------------------------------------------------------- 1 | import OAuth2Util 2 | import praw 3 | import psycopg2 4 | import re 5 | import json 6 | import requests 7 | import urllib.parse 8 | import urllib.request 9 | import os 10 | import sys 11 | from bs4 import BeautifulSoup 12 | 13 | try: 14 | user = 'TweakInfoBot' 15 | version = 'v71' 16 | userAgent = 'OS X 10.11 - com.tsunderedev.tweakinfo - ' + version + ' - detects tweaks mentioned in /r/jailbreak and /r/iOSthemes (by /u/hizinfiz) ' 17 | subreddit = 'jailbreak' 18 | subreddit2 = 'iOSthemes' 19 | admin = 'hizinfiz' 20 | except Exception as e: 21 | print (Exception, str(e)) 22 | 23 | replied = False 24 | 25 | footer = '\n---\n\n^(*bleep boop I\'m a bot*)\n\n^(Type the name of a tweak or theme enclosed in double brackets `[[tweak name]]` and I\'ll look it up for you.)\n\nI also reply to PMs!\n\n^[[Info](http://www.reddit.com/r/hizinfiz/wiki/TweakInfoBot)] ^[[Source](https://github.com/hizinfiz/TweakInfoBot)] ^[[Mistake?](http://www.reddit.com/message/compose/?to=' + admin + '&subject=%2Fu%2FTweakInfoBot%20feedback;message=If%20you%20are%20providing%20feedback%20about%20a%20specific%20post%2C%20please%20include%20the%20link%20to%20that%20post.%20Thanks!)]' 26 | 27 | urllib.parse.uses_netloc.append("postgres") 28 | url = urllib.parse.urlparse(os.environ["DATABASE_URL"]) 29 | 30 | db = psycopg2.connect( 31 | database=url.path[1:], 32 | user=url.username, 33 | password=url.password, 34 | host=url.hostname, 35 | port=url.port 36 | ) 37 | c = db.cursor() 38 | c.execute('CREATE TABLE IF NOT EXISTS comments (SUB TEXT, LAST TEXT)') 39 | c.execute('CREATE TABLE IF NOT EXISTS posts (SUB TEXT, LAST TEXT)') 40 | db.commit() 41 | db.close() 42 | 43 | # pattern = r'\[\[(\w\s)+\]\]' 44 | # pattern = r'\[\[([\S\s][^\[\]]*)+\]\]' 45 | # pattern = r'\[\[[\w\s`~!@#$%^&*()-_=+{}\\|;:'",<.>/?]+\]\]' 46 | pattern = r'\[\[[\w\s`~\!\@\#\$\%\^\&\*\(\)\-\_\=\+\{\}\\\|\;\:\'\"\,\<\.\>\/\?]+\]\]' 47 | spPattern = r'\[\[\s[\w`~\!\@\#\$\%\^\&\*\(\)\-\_\=\+\{\}\\\|\;\:\'\"\,\<\.\>\/\?]+\s\]\]' 48 | 49 | # Check new comments from subreddit for tweak queries 50 | def checkComments(sub): 51 | print (' Checking comments from /r/' + sub + '...') 52 | s = r.get_subreddit(sub) 53 | count = 0 54 | 55 | # records the first comment checked to use as a stopping point in the next run 56 | for com in s.get_comments(limit = 1): 57 | first = com.id 58 | 59 | # get the most recent comments posted to the subreddit 60 | for com in s.get_comments(limit = 250): 61 | comID = com.id 62 | comBody = com.body 63 | comAut = com.author 64 | 65 | print(' Comment from ' + str(comAut)) 66 | 67 | # ignored comments I posted 68 | if str(comAut) == 'TweakInfoBot': 69 | print(' I\'ll skip myself :P') 70 | else: 71 | # check if the current comment is stored in the database 72 | c.execute('SELECT * FROM comments WHERE LAST = %s', (comID,)) 73 | 74 | # If the comment is not in the database, check for tweak requests 75 | if c.fetchone() == None: 76 | search = re.search(pattern, comBody, re.I|re.M) 77 | 78 | # This is a requested addition... [[...]] matches but [[ ... ]] does not 79 | if search: 80 | search != re.search(spPattern, comBody, re.I|re.M) 81 | 82 | # Leave a comment if there was a request 83 | if search: 84 | message = '' 85 | # this iterates through all of the matches since there might be multiple within a single comment 86 | for match in re.finditer(pattern, comBody, re.I|re.M): 87 | tweak = match.group()[2:-2] 88 | tweak = getTweak(tweak) 89 | message += tweak 90 | 91 | # check if the comment has replies from TweakInfoBot... 92 | # This method is necessary because in the event that the comment stored from the previous run gets removed 93 | # by a mod, TweakInfoBot will recheck ALL of the comments, and in many cases TweakInfoBot will leave a 94 | # double or even triple reply. This coincidence surprisingly happened very often. 95 | cs = r.get_submission(com.permalink).comments[0] 96 | if cs.replies == []: # if there are no replies 97 | sendReply(message, com) # then leave a message 98 | else: 99 | for rep in cs.replies: # if there are replies, iterate through them 100 | if str(rep.author) == "TweakInfoBot": # and one of them is from TweakInfoBot 101 | replied = True # take note of that and don't reply 102 | print(' Already checked...') 103 | # if there are replies but TweakInfoBot has not yet replied, send a reply 104 | if replied == False: 105 | sendReply(message, com) 106 | # If a comment was already left, break out of replying to comments since they were checked the last run through 107 | else: 108 | print(' Already checked...') 109 | break 110 | 111 | replied = False 112 | 113 | # Update with the first checked comment for the next run through 114 | # c.execute('INSERT INTO comments VALUES (%s, %s)', [sub, first]) 115 | c.execute('UPDATE comments SET LAST = %s WHERE SUB = %s', [first, sub]) 116 | db.commit() 117 | 118 | # Check new posts from subreddit for tweak queries 119 | def checkPosts(sub): 120 | print (' Checking posts from /r/' + sub + '...') 121 | s = r.get_subreddit(sub) 122 | count = 0 123 | 124 | for pos in s.get_new(limit = 1): 125 | first = pos.id 126 | 127 | for pos in s.get_new(limit = 50): 128 | postID = pos.id 129 | postBody = pos.selftext 130 | postAut = pos.author 131 | 132 | print(' Post from ' + str(postAut)) 133 | 134 | c.execute('SELECT * FROM posts WHERE LAST = %s', (postID,)) 135 | 136 | # If a post was not yet left, check for tweak requests 137 | if c.fetchone() == None: 138 | search = re.search(pattern, postBody, re.I|re.M) 139 | 140 | if search: 141 | search != re.search(spPattern, postBody, re.I|re.M) 142 | 143 | # Leave a post if there was a request 144 | if search: 145 | message = '' 146 | for match in re.finditer(pattern, postBody, re.I|re.M): 147 | tweak = match.group()[2:-2] 148 | tweak = getTweak(tweak) 149 | message += tweak 150 | 151 | for com in pos.comments: 152 | if str(com.author) == 'TweakInfoBot': 153 | if com.is_root == True: 154 | print(' Already checked...') 155 | else: 156 | try: 157 | pos.add_comment(message+footer) 158 | except Exception as e: 159 | print (Exception, str(e)) 160 | # If a post was already left, break out of replying to posts since they were checked the last run through 161 | else: 162 | print(' Already checked...') 163 | break 164 | 165 | # Update with the first checked post for the next run through 166 | # c.execute('INSERT INTO posts VALUES (%s, %s)', [sub, first]) 167 | c.execute('UPDATE posts SET LAST = %s WHERE SUB = %s', [first, sub]) 168 | db.commit() 169 | 170 | # Checks the inbox for new messages 171 | def checkInbox(): 172 | pms = r.get_unread(update_user=True, limit=1000) 173 | message = '' 174 | 175 | for pm in pms: 176 | try: 177 | author = pm.author.name 178 | except: 179 | pm.mark_as_read() 180 | continue 181 | 182 | if not pm.was_comment: 183 | print(' Message from ' + author + '...') 184 | 185 | search = re.search(pattern, pm.body.lower(), re.I|re.M) 186 | 187 | if search: 188 | for match in re.finditer(pattern, pm.body.lower(), re.I|re.M): 189 | tweak = match.group()[2:-2] 190 | tweak = getTweak(tweak) 191 | message += tweak 192 | 193 | else: 194 | print(' Forwarding message to admin...') 195 | r.send_message(admin, 'PM from /u/' + author, 'Message from /u/' + author + '\n\nSubject: ' + pm.subject + '\n\n---\n\n' + pm.body + footer) 196 | message = 'I am a bot *bleep bloop*\n\nI received your message and I forwarded it to /u/' + admin + ' who will take a look at it when he gets a chance.\n\nIf it\'s urgent, you should PM them directly.' 197 | 198 | message += footer 199 | 200 | print (' Replying to message from ' + author + '...') 201 | pm.reply(message) 202 | else: 203 | print(' Comment from ' + author + '...') 204 | print(' Forwarding message to admin...') 205 | r.send_message(admin, 'Comment from /u/' + author, 'Message from /u/' + author + '\n\nSubject: ' + pm.subject + '\n\nContext: ' + pm.context + '\n\n---\n\n' + pm.body) 206 | 207 | pm.mark_as_read() 208 | time.sleep(1) 209 | 210 | # Send a reply, moved this into its own method because it showed up several times 211 | def sendReply(message, com): 212 | try: 213 | com.reply(message + footer) 214 | except Exception as e: 215 | print (Exception, str(e)) 216 | 217 | # Tries to find information about a tweak 218 | def getTweak(tweak): 219 | print (' Getting info for ' + tweak) 220 | tweakRemoveTrailing = removeTrailing(tweak).strip().lower() 221 | tweakNoSpace = tweak.replace(' ', '').lower() 222 | msg = '* **' + tweak + '** - Could not find info about this tweak/theme\n' 223 | base = 'https://cydia.saurik.com/api/macciti?query=' 224 | query = [base+tweak, base+tweakNoSpace] 225 | 226 | for q in query: 227 | r = requests.get(q) 228 | j = r.json() 229 | d = json.dumps(j) 230 | d = json.loads(d) 231 | 232 | for twk in d['results']: 233 | name = twk['display'] 234 | 235 | # if there is an exact match 236 | if tweak == name: 237 | msg = genMessage(twk) 238 | return msg 239 | # if the match is not exact 240 | else: 241 | name = removeTrailing(name).strip().lower() 242 | nameNoSpace = name.replace(' ', '').lower() 243 | 244 | if (name == tweakRemoveTrailing) | (name == tweakNoSpace) | (nameNoSpace == tweakRemoveTrailing) | (nameNoSpace == tweakNoSpace): 245 | msg = '* [**' + tweak + '**]' + genMessage(twk) 246 | return msg 247 | 248 | return msg 249 | 250 | # Get the price of a tweak 251 | def getPrice(package): 252 | print (' Getting price for ' + package) 253 | base = 'http://cydia.saurik.com/api/ibbignerd?query=' 254 | query = base+package 255 | 256 | r = requests.get(query) 257 | j = r.json() 258 | 259 | if j == None: 260 | return 'Free' 261 | else: 262 | d = json.dumps(j) 263 | d = json.loads(d) 264 | 265 | price = '$' + str(d['msrp']) 266 | 267 | return price 268 | 269 | # Get the repo of a tweak 270 | def getRepo(link): 271 | html = urllib.request.urlopen(link) 272 | soup = BeautifulSoup (html, "html.parser") 273 | 274 | # All of the default repos have this CSS class towards the bottom of their depiction 275 | repo = soup.find('span', {'class' : 'source-name'}).contents[0] 276 | 277 | if repo == 'ModMyi.com': repo = 'ModMyi' 278 | 279 | return repo 280 | 281 | # Generate the tweak info message (reddit formatted) 282 | def genMessage(twk): 283 | print(' Found it!') 284 | link = 'http://cydia.saurik.com/package/' + str(twk['name']) 285 | rep = getRepo(link) 286 | typ = str(twk['section']) 287 | cos = getPrice(twk['name']) 288 | des = str(twk['summary']) 289 | 290 | msg = '(' + link + ') -' + rep + ', ' + cos + ' | ' + typ + ' | ' + des + '\n' 291 | 292 | return msg 293 | 294 | # Some packages are named "Tweak [iOS ...]" or similar, this removes all the excess 295 | def removeTrailing(name): 296 | separator = ['(','[','-','for'] 297 | 298 | for sep in separator: 299 | head, div, tail = name.partition(sep) 300 | 301 | if div is not '': 302 | return head 303 | 304 | return head 305 | 306 | if __name__ == '__main__': 307 | if not userAgent: 308 | print('Missing User Agent') 309 | else: 310 | print('Logging in...') 311 | r = praw.Reddit(userAgent + version) 312 | o = OAuth2Util.OAuth2Util(r) 313 | o.refresh(force=True) 314 | print('Logged in!') 315 | 316 | # sub = r.get_subreddit(subreddit) 317 | 318 | print('Connecting to database...') 319 | db = psycopg2.connect( 320 | database=url.path[1:], 321 | user=url.username, 322 | password=url.password, 323 | host=url.hostname, 324 | port=url.port 325 | ) 326 | c = db.cursor() 327 | print('Connected to database!') 328 | 329 | print('Start TweakInfoBot for /r/' + subreddit + ' and /r/' + subreddit2) 330 | 331 | if len(sys.argv) > 1: 332 | if sys.argv[1] == 'JBcom': 333 | print(' RUNNING JBCOM') 334 | checkPosts(subreddit) 335 | checkComments(subreddit) 336 | if sys.argv[1] == 'ITcom': 337 | print(' RUNNING ITCOM') 338 | checkPosts(subreddit2) 339 | checkComments(subreddit2) 340 | if sys.argv[1] == 'test': 341 | print(' RUNNING TEST') 342 | checkPosts('hizinfiz') 343 | checkComments('hizinfiz') 344 | if sys.argv[1] == 'inbox': 345 | print(' RUNNING INBOX') 346 | checkInbox() 347 | 348 | print('End TweakInfoBot for /r/' + subreddit + ' and /r/' + subreddit2) 349 | 350 | db.close() --------------------------------------------------------------------------------