├── .gitattributes ├── .gitignore ├── Database scripts ├── LICENSE ├── README.md ├── remindmebot-example.cfg ├── remindmebot_reply.py ├── remindmebot_search.py └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /Database scripts: -------------------------------------------------------------------------------- 1 | CREATE TABLE `comment_list` ( 2 | `list` longtext, 3 | `id` int(11) NOT NULL AUTO_INCREMENT, 4 | PRIMARY KEY (`id`) 5 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1 6 | 7 | CREATE TABLE `message_date` ( 8 | `id` int(11) NOT NULL AUTO_INCREMENT, 9 | `permalink` varchar(400) NOT NULL DEFAULT '', 10 | `message` varchar(11000) DEFAULT NULL, 11 | `new_date` datetime DEFAULT NULL, 12 | `origin_date` datetime DEFAULT NULL, 13 | `userID` varchar(50) DEFAULT NULL, 14 | PRIMARY KEY (`id`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1 16 | 17 | ALTER TABLE message_date ADD COLUMN `origin_date` datetime DEFAULT NULL AFTER new_date; 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Giuseppe Ranieri 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is the point of RemindMeBot? 2 | 3 | RemindMeBot was made as a way to remind the user about a comment or thread for later use. 4 | For example, someone on AskReddit posts a list of cool movies to watch. Although you don't have time to watch it now, you make a comment to remember to view a movie from the list later. 5 | However, you forget about the comment you made because you always find the cool things before sleeping and you have a short attention span to view your previously made comments. 6 | Think of RemindMe! as an improved and automatic "saved" for later 7 | 8 | 9 | For more info: http://www.reddit.com/r/RemindMeBot/comments/24duzp/remindmebot_info/ -------------------------------------------------------------------------------- /remindmebot-example.cfg: -------------------------------------------------------------------------------- 1 | [Reddit] 2 | username = botname 3 | password = botpassword 4 | client_id = id 5 | client_secret = secert shh 6 | redirect_uri = http://127.0.0.1:65010/ 7 | 8 | [SQL] 9 | user = user 10 | passwd = password -------------------------------------------------------------------------------- /remindmebot_reply.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | # ============================================================================= 4 | # IMPORTS 5 | # ============================================================================= 6 | 7 | import praw 8 | import OAuth2Util 9 | import re 10 | import MySQLdb 11 | import ConfigParser 12 | import time 13 | from datetime import datetime, timedelta 14 | from requests.exceptions import HTTPError, ConnectionError, Timeout 15 | from praw.errors import ExceptionList, APIException, InvalidCaptcha, InvalidUser, RateLimitExceeded 16 | from socket import timeout 17 | from pytz import timezone 18 | 19 | # ============================================================================= 20 | # GLOBALS 21 | # ============================================================================= 22 | 23 | # Reads the config file 24 | config = ConfigParser.ConfigParser() 25 | config.read("remindmebot.cfg") 26 | 27 | #Reddit info 28 | reddit = praw.Reddit("RemindMeB0tReply") 29 | o = OAuth2Util.OAuth2Util(reddit, print_log=True) 30 | o.refresh(force=True) 31 | # DB Info 32 | DB_USER = config.get("SQL", "user") 33 | DB_PASS = config.get("SQL", "passwd") 34 | 35 | # ============================================================================= 36 | # CLASSES 37 | # ============================================================================= 38 | 39 | class Connect(object): 40 | """ 41 | DB connection class 42 | """ 43 | connection = None 44 | cursor = None 45 | 46 | def __init__(self): 47 | self.connection = MySQLdb.connect( 48 | host="localhost", user=DB_USER, passwd=DB_PASS, db="bot" 49 | ) 50 | self.cursor = self.connection.cursor() 51 | 52 | class Reply(object): 53 | 54 | def __init__(self): 55 | self._queryDB = Connect() 56 | self._replyMessage =( 57 | "RemindMeBot private message here!" 58 | "\n\n**The message:** \n\n>{message}" 59 | "\n\n**The original comment:** \n\n>{original}" 60 | "\n\n**The parent comment from the original comment or its submission:** \n\n>{parent}" 61 | "{origin_date_text}" 62 | "\n\n#Would you like to be reminded of the original comment again? Just set your time again after the RemindMe! command. [CLICK HERE]" 63 | "(http://np.reddit.com/message/compose/?to=RemindMeBot&subject=Reminder&message=[{original}]" 64 | "%0A%0ARemindMe!)" 65 | "\n\n_____\n\n" 66 | "|[^(FAQs)](http://np.reddit.com/r/RemindMeBot/comments/24duzp/remindmebot_info/)" 67 | "|[^(Custom)](http://np.reddit.com/message/compose/?to=RemindMeBot&subject=Reminder&message=" 68 | "[LINK INSIDE SQUARE BRACKETS else default to FAQs]%0A%0A" 69 | "NOTE: Don't forget to add the time options after the command.%0A%0ARemindMe!)" 70 | "|[^(Your Reminders)](http://np.reddit.com/message/compose/?to=RemindMeBot&subject=List Of Reminders&message=MyReminders!)" 71 | "|[^(Feedback)](http://np.reddit.com/message/compose/?to=RemindMeBotWrangler&subject=Feedback)" 72 | "|[^(Code)](https://github.com/SIlver--/remindmebot-reddit)" 73 | "|[^(Browser Extensions)](https://np.reddit.com/r/RemindMeBot/comments/4kldad/remindmebot_extensions/)" 74 | "\n|-|-|-|-|-|-|" 75 | ) 76 | 77 | def parent_comment(self, dbPermalink): 78 | """ 79 | Returns the parent comment or if it's a top comment 80 | return the original submission 81 | """ 82 | try: 83 | commentObj = reddit.get_submission(_force_utf8(dbPermalink)).comments[0] 84 | if commentObj.is_root: 85 | return _force_utf8(commentObj.submission.permalink) 86 | else: 87 | return _force_utf8(reddit.get_info(thing_id=commentObj.parent_id).permalink) 88 | except IndexError as err: 89 | print "parrent_comment error" 90 | return "It seems your original comment was deleted, unable to return parent comment." 91 | # Catch any URLs that are not reddit comments 92 | except Exception as err: 93 | print "HTTPError/PRAW parent comment" 94 | return "Parent comment not required for this URL." 95 | 96 | def time_to_reply(self): 97 | """ 98 | Checks to see through SQL if net_date is < current time 99 | """ 100 | 101 | # get current time to compare 102 | currentTime = datetime.now(timezone('UTC')) 103 | currentTime = format(currentTime, '%Y-%m-%d %H:%M:%S') 104 | cmd = "SELECT * FROM message_date WHERE new_date < %s" 105 | self._queryDB.cursor.execute(cmd, [currentTime]) 106 | 107 | def search_db(self): 108 | """ 109 | Loop through data looking for which comments are old 110 | """ 111 | 112 | data = self._queryDB.cursor.fetchall() 113 | alreadyCommented = [] 114 | for row in data: 115 | # checks to make sure ID hasn't been commented already 116 | # For situtations where errors happened 117 | if row[0] not in alreadyCommented: 118 | flagDelete = False 119 | # MySQl- permalink, message, origin date, reddit user 120 | flagDelete = self.new_reply(row[1],row[2], row[4], row[5]) 121 | # removes row based on flagDelete 122 | if flagDelete: 123 | cmd = "DELETE FROM message_date WHERE id = %s" 124 | self._queryDB.cursor.execute(cmd, [row[0]]) 125 | self._queryDB.connection.commit() 126 | alreadyCommented.append(row[0]) 127 | 128 | self._queryDB.connection.commit() 129 | self._queryDB.connection.close() 130 | 131 | def new_reply(self, permalink, message, origin_date, author): 132 | """ 133 | Replies a second time to the user after a set amount of time 134 | """ 135 | """ 136 | print self._replyMessage.format( 137 | message, 138 | permalink 139 | ) 140 | """ 141 | print "---------------" 142 | print author 143 | print permalink 144 | 145 | origin_date_text = "" 146 | # Before feature was implemented, there are no origin dates stored 147 | if origin_date is not None: 148 | origin_date_text = ("\n\nYou requested this reminder on: " 149 | "[**" + _force_utf8(origin_date) + " UTC**](http://www.wolframalpha.com/input/?i=" 150 | + _force_utf8(origin_date) + " UTC To Local Time)") 151 | 152 | try: 153 | reddit.send_message( 154 | recipient=str(author), 155 | subject='Hello, ' + _force_utf8(str(author)) + ' RemindMeBot Here!', 156 | message=self._replyMessage.format( 157 | message=_force_utf8(message), 158 | original=_force_utf8(permalink), 159 | parent= self.parent_comment(permalink), 160 | origin_date_text = origin_date_text 161 | )) 162 | print "Did It" 163 | return True 164 | except InvalidUser as err: 165 | print "InvalidUser", err 166 | return True 167 | except APIException as err: 168 | print "APIException", err 169 | return False 170 | except IndexError as err: 171 | print "IndexError", err 172 | return False 173 | except (HTTPError, ConnectionError, Timeout, timeout) as err: 174 | print "HTTPError", err 175 | time.sleep(10) 176 | return False 177 | except RateLimitExceeded as err: 178 | print "RateLimitExceeded", err 179 | time.sleep(10) 180 | return False 181 | except praw.errors.HTTPException as err: 182 | print"praw.errors.HTTPException" 183 | time.sleep(10) 184 | return False 185 | 186 | """ 187 | From Reddit's Code 188 | https://github.com/reddit/reddit/blob/master/r2/r2/lib/unicode.py 189 | Brought to attention thanks to /u/13steinj 190 | """ 191 | def _force_unicode(text): 192 | 193 | if text == None: 194 | return u'' 195 | 196 | if isinstance(text, unicode): 197 | return text 198 | 199 | try: 200 | text = unicode(text, 'utf-8') 201 | except UnicodeDecodeError: 202 | text = unicode(text, 'latin1') 203 | except TypeError: 204 | text = unicode(text) 205 | return text 206 | 207 | 208 | def _force_utf8(text): 209 | return str(_force_unicode(text).encode('utf8')) 210 | 211 | 212 | # ============================================================================= 213 | # MAIN 214 | # ============================================================================= 215 | 216 | def main(): 217 | while True: 218 | checkReply = Reply() 219 | checkReply.time_to_reply() 220 | checkReply.search_db() 221 | time.sleep(10) 222 | 223 | 224 | # ============================================================================= 225 | # RUNNER 226 | # ============================================================================= 227 | print "start" 228 | if __name__ == '__main__': 229 | main() -------------------------------------------------------------------------------- /remindmebot_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | # ============================================================================= 4 | # IMPORTS 5 | # ============================================================================= 6 | import traceback 7 | import praw 8 | import OAuth2Util 9 | import re 10 | import MySQLdb 11 | import ConfigParser 12 | import ast 13 | import time 14 | import urllib 15 | import requests 16 | import parsedatetime.parsedatetime as pdt 17 | from datetime import datetime, timedelta 18 | from requests.exceptions import HTTPError, ConnectionError, Timeout 19 | from praw.errors import ExceptionList, APIException, InvalidCaptcha, InvalidUser, RateLimitExceeded, Forbidden 20 | from socket import timeout 21 | from pytz import timezone 22 | from threading import Thread 23 | 24 | # ============================================================================= 25 | # GLOBALS 26 | # ============================================================================= 27 | 28 | # Reads the config file 29 | config = ConfigParser.ConfigParser() 30 | config.read("remindmebot.cfg") 31 | 32 | #Reddit info 33 | reddit = praw.Reddit(user_agent= "RemindMes") 34 | o = OAuth2Util.OAuth2Util(reddit, print_log = True) 35 | o.refresh(force=True) 36 | 37 | DB_USER = config.get("SQL", "user") 38 | DB_PASS = config.get("SQL", "passwd") 39 | 40 | # Time when program was started 41 | START_TIME = time.time() 42 | # ============================================================================= 43 | # CLASSES 44 | # ============================================================================= 45 | 46 | class Connect(object): 47 | """ 48 | DB connection class 49 | """ 50 | connection = None 51 | cursor = None 52 | 53 | def __init__(self): 54 | self.connection = MySQLdb.connect( 55 | host="localhost", user=DB_USER, passwd=DB_PASS, db="bot" 56 | ) 57 | self.cursor = self.connection.cursor() 58 | 59 | class Search(object): 60 | commented = [] # comments already replied to 61 | subId = [] # reddit threads already replied in 62 | 63 | # Fills subId with previous threads. Helpful for restarts 64 | database = Connect() 65 | cmd = "SELECT list FROM comment_list WHERE id = 1" 66 | database.cursor.execute(cmd) 67 | data = database.cursor.fetchall() 68 | subId = ast.literal_eval("[" + data[0][0] + "]") 69 | database.connection.commit() 70 | database.connection.close() 71 | 72 | endMessage = ( 73 | "\n\n_____\n\n" 74 | "|[^(FAQs)](http://np.reddit.com/r/RemindMeBot/comments/24duzp/remindmebot_info/)" 75 | "|[^(Custom)](http://np.reddit.com/message/compose/?to=RemindMeBot&subject=Reminder&message=" 76 | "[LINK INSIDE SQUARE BRACKETS else default to FAQs]%0A%0A" 77 | "NOTE: Don't forget to add the time options after the command.%0A%0ARemindMe!)" 78 | "|[^(Your Reminders)](http://np.reddit.com/message/compose/?to=RemindMeBot&subject=List Of Reminders&message=MyReminders!)" 79 | "|[^(Feedback)](http://np.reddit.com/message/compose/?to=RemindMeBotWrangler&subject=Feedback)" 80 | "|[^(Code)](https://github.com/SIlver--/remindmebot-reddit)" 81 | "|[^(Browser Extensions)](https://np.reddit.com/r/RemindMeBot/comments/4kldad/remindmebot_extensions/)" 82 | "\n|-|-|-|-|-|-|" 83 | ) 84 | 85 | def __init__(self, comment): 86 | self._addToDB = Connect() 87 | self.comment = comment # Reddit comment Object 88 | self._messageInput = '"Hello, I\'m here to remind you to see the parent comment!"' 89 | self._storeTime = None 90 | self._replyMessage = "" 91 | self._replyDate = None 92 | self._originDate = datetime.fromtimestamp(comment.created_utc) 93 | self._privateMessage = False 94 | 95 | def run(self, privateMessage=False): 96 | self._privateMessage = privateMessage 97 | self.parse_comment() 98 | self.save_to_db() 99 | self.build_message() 100 | self.reply() 101 | if self._privateMessage == True: 102 | # Makes sure to marks as read, even if the above doesn't work 103 | self.comment.mark_as_read() 104 | self.find_bot_child_comment() 105 | self._addToDB.connection.close() 106 | 107 | def parse_comment(self): 108 | """ 109 | Parse comment looking for the message and time 110 | """ 111 | 112 | if self._privateMessage == True: 113 | permalinkTemp = re.search('\[(.*?)\]', self.comment.body) 114 | if permalinkTemp: 115 | self.comment.permalink = permalinkTemp.group()[1:-1] 116 | # Makes sure the URL is real 117 | try: 118 | urllib.urlopen(self.comment.permalink) 119 | except IOError: 120 | self.comment.permalink = "http://np.reddit.com/r/RemindMeBot/comments/24duzp/remindmebot_info/" 121 | else: 122 | # Defaults when the user doesn't provide a link 123 | self.comment.permalink = "http://np.reddit.com/r/RemindMeBot/comments/24duzp/remindmebot_info/" 124 | 125 | # remove RemindMe! or !RemindMe (case insenstive) 126 | match = re.search(r'(?i)(!*)RemindMe(!*)', self.comment.body) 127 | # and everything before 128 | tempString = self.comment.body[match.start():] 129 | 130 | # remove all format breaking characters IE: [ ] ( ) newline 131 | tempString = tempString.split("\n")[0] 132 | # adds " at the end if only 1 exists 133 | if (tempString.count('"') == 1): 134 | tempString = tempString + '"' 135 | 136 | # Use message default if not found 137 | messageInputTemp = re.search('(["].{0,9000}["])', tempString) 138 | if messageInputTemp: 139 | self._messageInput = messageInputTemp.group() 140 | # Fix issue with dashes for parsedatetime lib 141 | tempString = tempString.replace('-', "/") 142 | # Remove RemindMe! 143 | self._storeTime = re.sub('(["].{0,9000}["])', '', tempString)[9:] 144 | 145 | def save_to_db(self): 146 | """ 147 | Saves the permalink comment, the time, and the message to the DB 148 | """ 149 | 150 | cal = pdt.Calendar() 151 | try: 152 | holdTime = cal.parse(self._storeTime, datetime.now(timezone('UTC'))) 153 | except ValueError, OverflowError: 154 | # year too long 155 | holdTime = cal.parse("9999-12-31") 156 | if holdTime[1] == 0: 157 | # default time 158 | holdTime = cal.parse("1 day", datetime.now(timezone('UTC'))) 159 | self._replyMessage = "**Defaulted to one day.**\n\n" 160 | # Converting time 161 | #9999/12/31 HH/MM/SS 162 | self._replyDate = time.strftime('%Y-%m-%d %H:%M:%S', holdTime[0]) 163 | cmd = "INSERT INTO message_date (permalink, message, new_date, origin_date, userID) VALUES (%s, %s, %s, %s, %s)" 164 | self._addToDB.cursor.execute(cmd, ( 165 | self.comment.permalink.encode('utf-8'), 166 | self._messageInput.encode('utf-8'), 167 | self._replyDate, 168 | self._originDate, 169 | self.comment.author)) 170 | self._addToDB.connection.commit() 171 | # Info is added to DB, user won't be bothered a second time 172 | self.commented.append(self.comment.id) 173 | 174 | def build_message(self): 175 | """ 176 | Buildng message for user 177 | """ 178 | permalink = self.comment.permalink 179 | self._replyMessage +=( 180 | "I will be messaging you on [**{0} UTC**](http://www.wolframalpha.com/input/?i={0} UTC To Local Time)" 181 | " to remind you of [**this link.**]({commentPermalink})" 182 | "{remindMeMessage}") 183 | 184 | try: 185 | self.sub = reddit.get_submission(self.comment.permalink) 186 | except Exception as err: 187 | print "link had http" 188 | if self._privateMessage == False and self.sub.id not in self.subId: 189 | remindMeMessage = ( 190 | "\n\n[**CLICK THIS LINK**](http://np.reddit.com/message/compose/?to=RemindMeBot&subject=Reminder&message=" 191 | "[{permalink}]%0A%0ARemindMe! {time}) to send a PM to also be reminded and to reduce spam." 192 | "\n\n^(Parent commenter can ) [^(delete this message to hide from others.)]" 193 | "(http://np.reddit.com/message/compose/?to=RemindMeBot&subject=Delete Comment&message=Delete! ____id____)").format( 194 | permalink=permalink, 195 | time=self._storeTime.replace('\n', '') 196 | ) 197 | else: 198 | remindMeMessage = "" 199 | 200 | self._replyMessage = self._replyMessage.format( 201 | self._replyDate, 202 | remindMeMessage=remindMeMessage, 203 | commentPermalink=permalink) 204 | self._replyMessage += Search.endMessage 205 | 206 | def reply(self): 207 | """ 208 | Messages the user letting as a confirmation 209 | """ 210 | 211 | author = self.comment.author 212 | def send_message(): 213 | reddit.send_message(author, 'Hello, ' + str(author) + ' RemindMeBot Confirmation Sent', self._replyMessage) 214 | 215 | try: 216 | if self._privateMessage == False: 217 | # First message will be a reply in a thread 218 | # afterwards are PM in the same thread 219 | if (self.sub.id not in self.subId): 220 | newcomment = self.comment.reply(self._replyMessage) 221 | self.subId.append(self.sub.id) 222 | # adding it to database as well 223 | database = Connect() 224 | insertsubid = ", \'" + self.sub.id + "\'" 225 | cmd = 'UPDATE comment_list set list = CONCAT(list, "{0}") where id = 1'.format(insertsubid) 226 | database.cursor.execute(cmd) 227 | database.connection.commit() 228 | database.connection.close() 229 | # grabbing comment just made 230 | reddit.get_info( 231 | thing_id='t1_'+str(newcomment.id) 232 | # edit comment with self ID so it can be deleted 233 | ).edit(self._replyMessage.replace('____id____', str(newcomment.id))) 234 | else: 235 | send_message() 236 | else: 237 | print str(author) 238 | send_message() 239 | except RateLimitExceeded as err: 240 | print err 241 | # PM when I message too much 242 | send_message() 243 | time.sleep(10) 244 | except Forbidden as err: 245 | send_message() 246 | except APIException as err: # Catch any less specific API errors 247 | print err 248 | #else: 249 | #print self._replyMessage 250 | 251 | def find_bot_child_comment(self): 252 | """ 253 | Finds the remindmebot comment in the child 254 | """ 255 | try: 256 | # Grabbing all child comments 257 | replies = reddit.get_submission(url=self.comment.permalink).comments[0].replies 258 | # Look for bot's reply 259 | commentfound = "" 260 | if replies: 261 | for comment in replies: 262 | if str(comment.author) == "RemindMeBot": 263 | commentfound = comment 264 | self.comment_count(commentfound) 265 | except Exception as err: 266 | pass 267 | 268 | def comment_count(self, commentfound): 269 | """ 270 | Posts edits the count if found 271 | """ 272 | query = "SELECT count(DISTINCT userid) FROM message_date WHERE permalink = %s" 273 | self._addToDB.cursor.execute(query, [self.comment.permalink]) 274 | data = self._addToDB.cursor.fetchall() 275 | # Grabs the tuple within the tuple, a number/the dbcount 276 | dbcount = count = str(data[0][0]) 277 | comment = reddit.get_info(thing_id='t1_'+str(commentfound.id)) 278 | body = comment.body 279 | 280 | pattern = r'(\d+ OTHERS |)CLICK(ED|) THIS LINK' 281 | # Compares to see if current number is bigger 282 | # Useful for after some of the reminders are sent, 283 | # a smaller number doesnt overwrite bigger 284 | try: 285 | currentcount = int(re.search(r'\d+', re.search(pattern, body).group(0)).group()) 286 | # for when there is no number 287 | except AttributeError as err: 288 | currentcount = 0 289 | if currentcount > int(dbcount): 290 | count = str(currentcount + 1) 291 | # Adds the count to the post 292 | body = re.sub( 293 | pattern, 294 | count + " OTHERS CLICKED THIS LINK", 295 | body) 296 | comment.edit(body) 297 | def grab_list_of_reminders(username): 298 | """ 299 | Grabs all the reminders of the user 300 | """ 301 | database = Connect() 302 | query = "SELECT permalink, message, new_date, id FROM message_date WHERE userid = %s ORDER BY new_date" 303 | database.cursor.execute(query, [username]) 304 | data = database.cursor.fetchall() 305 | table = ( 306 | "[**Click here to delete all your reminders at once quickly.**]" 307 | "(http://np.reddit.com/message/compose/?to=RemindMeBot&subject=Reminder&message=RemoveAll!)\n\n" 308 | "|Permalink|Message|Date|Remove|\n" 309 | "|-|-|-|:-:|") 310 | for row in data: 311 | date = str(row[2]) 312 | table += ( 313 | "\n|" + row[0] + "|" + row[1] + "|" + 314 | "[" + date + " UTC](http://www.wolframalpha.com/input/?i=" + str(row[2]) + " UTC to local time)|" 315 | "[[X]](https://np.reddit.com/message/compose/?to=RemindMeBot&subject=Remove&message=Remove!%20"+ str(row[3]) + ")|" 316 | ) 317 | if len(data) == 0: 318 | table = "Looks like you have no reminders. Click the **[Custom]** button below to make one!" 319 | elif len(table) > 9000: 320 | table = "Sorry the comment was too long to display. Message /u/RemindMeBotWrangler as this was his lazy error catching." 321 | table += Search.endMessage 322 | return table 323 | 324 | def remove_reminder(username, idnum): 325 | """ 326 | Deletes the reminder from the database 327 | """ 328 | database = Connect() 329 | # only want userid to confirm if owner 330 | query = "SELECT userid FROM message_date WHERE id = %s" 331 | database.cursor.execute(query, [idnum]) 332 | data = database.cursor.fetchall() 333 | deleteFlag = False 334 | for row in data: 335 | userid = str(row[0]) 336 | # If the wrong ID number is given, item isn't deleted 337 | if userid == username: 338 | cmd = "DELETE FROM message_date WHERE id = %s" 339 | database.cursor.execute(cmd, [idnum]) 340 | deleteFlag = True 341 | 342 | 343 | database.connection.commit() 344 | return deleteFlag 345 | 346 | def remove_all(username): 347 | """ 348 | Deletes all reminders at once 349 | """ 350 | database = Connect() 351 | query = "SELECT * FROM message_date where userid = %s" 352 | database.cursor.execute(query, [username]) 353 | count = len(database.cursor.fetchall()) 354 | cmd = "DELETE FROM message_date WHERE userid = %s" 355 | database.cursor.execute(cmd, [username]) 356 | database.connection.commit() 357 | 358 | return count 359 | 360 | def read_pm(): 361 | try: 362 | for message in reddit.get_unread(unset_has_mail=True, update_user=True, limit = 100): 363 | # checks to see as some comments might be replys and non PMs 364 | prawobject = isinstance(message, praw.objects.Message) 365 | if (("remindme" in message.body.lower() or 366 | "remindme!" in message.body.lower() or 367 | "!remindme" in message.body.lower()) and prawobject): 368 | redditPM = Search(message) 369 | redditPM.run(privateMessage=True) 370 | message.mark_as_read() 371 | elif (("delete!" in message.body.lower() or "!delete" in message.body.lower()) and prawobject): 372 | givenid = re.findall(r'delete!\s(.*?)$', message.body.lower())[0] 373 | givenid = 't1_'+givenid 374 | comment = reddit.get_info(thing_id=givenid) 375 | try: 376 | parentcomment = reddit.get_info(thing_id=comment.parent_id) 377 | if message.author.name == parentcomment.author.name: 378 | comment.delete() 379 | except ValueError as err: 380 | # comment wasn't inside the list 381 | pass 382 | except AttributeError as err: 383 | # comment might be deleted already 384 | pass 385 | message.mark_as_read() 386 | elif (("myreminders!" in message.body.lower() or "!myreminders" in message.body.lower()) and prawobject): 387 | listOfReminders = grab_list_of_reminders(message.author.name) 388 | message.reply(listOfReminders) 389 | message.mark_as_read() 390 | elif (("remove!" in message.body.lower() or "!remove" in message.body.lower()) and prawobject): 391 | givenid = re.findall(r'remove!\s(.*?)$', message.body.lower())[0] 392 | deletedFlag = remove_reminder(message.author.name, givenid) 393 | listOfReminders = grab_list_of_reminders(message.author.name) 394 | # This means the user did own that reminder 395 | if deletedFlag == True: 396 | message.reply("Reminder deleted. Your current Reminders:\n\n" + listOfReminders) 397 | else: 398 | message.reply("Try again with the current IDs that belong to you below. Your current Reminders:\n\n" + listOfReminders) 399 | message.mark_as_read() 400 | elif (("removeall!" in message.body.lower() or "!removeall" in message.body.lower()) and prawobject): 401 | count = str(remove_all(message.author.name)) 402 | listOfReminders = grab_list_of_reminders(message.author.name) 403 | message.reply("I have deleted all **" + count + "** reminders for you.\n\n" + listOfReminders) 404 | message.mark_as_read() 405 | except Exception as err: 406 | print traceback.format_exc() 407 | 408 | def check_comment(comment): 409 | """ 410 | Checks the body of the comment, looking for the command 411 | """ 412 | redditCall = Search(comment) 413 | if (("remindme!" in comment.body.lower() or 414 | "!remindme" in comment.body.lower()) and 415 | redditCall.comment.id not in redditCall.commented and 416 | 'RemindMeBot' != str(comment.author) and 417 | START_TIME < redditCall.comment.created_utc): 418 | print "in" 419 | t = Thread(target=redditCall.run()) 420 | t.start() 421 | 422 | def check_own_comments(): 423 | user = reddit.get_redditor("RemindMeBot") 424 | for comment in user.get_comments(limit=None): 425 | if comment.score <= -5: 426 | print "COMMENT DELETED" 427 | print comment 428 | comment.delete() 429 | # ============================================================================= 430 | # MAIN 431 | # ============================================================================= 432 | 433 | def main(): 434 | print "start" 435 | checkcycle = 0 436 | while True: 437 | try: 438 | # grab the request 439 | request = requests.get('https://api.pushshift.io/reddit/search?q=%22RemindMe%22&limit=100', 440 | headers = {'User-Agent': 'RemindMeBot-Agent'}) 441 | json = request.json() 442 | comments = json["data"] 443 | read_pm() 444 | for rawcomment in comments: 445 | # object constructor requires empty attribute 446 | rawcomment['_replies'] = '' 447 | comment = praw.objects.Comment(reddit, rawcomment) 448 | check_comment(comment) 449 | 450 | # Only check periodically 451 | if checkcycle >= 5: 452 | check_own_comments() 453 | checkcycle = 0 454 | else: 455 | checkcycle += 1 456 | 457 | print "----" 458 | time.sleep(30) 459 | except Exception as err: 460 | print traceback.format_exc() 461 | time.sleep(30) 462 | """ 463 | Will add later if problem with api.pushshift 464 | hence why check_comment is a function 465 | try: 466 | for comment in praw.helpers.comment_stream(reddit, 'all', limit = 1, verbosity = 0): 467 | check_comment(comment) 468 | except Exception as err: 469 | print err 470 | """ 471 | # ============================================================================= 472 | # RUNNER 473 | # ============================================================================= 474 | 475 | if __name__ == '__main__': 476 | main() 477 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Important, this libary only goes up to praw==3.6.0. As version 4+ changed how the library calls the API. --------------------------------------------------------------------------------