diff --git a/.gitignore b/.gitignore index fd7037d0..8b72d52a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *.bak *.pyc +.DS_Store +.venv +*.swp *.pyo *.secret *.swp @@ -7,6 +10,7 @@ *~ .DS_Store .vagrant/ +redditbot_replied.json .venv __pycache__ github_pat.secret diff --git a/chaos.py b/chaos.py index ebe24a4f..53f44ab4 100644 --- a/chaos.py +++ b/chaos.py @@ -38,6 +38,7 @@ class LessThanFilter(logging.Filter): """ Source: https://stackoverflow.com/questions/2302315 """ + def __init__(self, exclusive_maximum, name=""): super(LessThanFilter, self).__init__(name) self.max_level = exclusive_maximum @@ -58,10 +59,14 @@ def main(): logging_handler_err = logging.StreamHandler(sys.stderr) logging_handler_err.setLevel(settings.LOG_LEVEL_ERR) - logging.basicConfig(level=logging.NOTSET, - format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - datefmt='%m-%d %H:%M', - handlers=[logging_handler_out, logging_handler_err]) + logging.basicConfig( + level=logging.NOTSET, + format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + datefmt='%m-%d %H:%M', + handlers=[ + logging_handler_out, + logging_handler_err + ]) logging.getLogger("requests").propagate = False logging.getLogger("sh").propagate = False @@ -91,6 +96,7 @@ def main(): " - starting up and entering event loop", api_twitter.GetApi()) + os.system("pkill chaos_redditbot") os.system("pkill uwsgi") subprocess.Popen(["/root/.virtualenvs/chaos/bin/uwsgi", @@ -100,10 +106,14 @@ def main(): "--check-static", "/root/workspace/Chaos/server/", "--daemonize", "/root/workspace/Chaos/log/uwsgi.log"]) + # Start Reddit bot + subprocess.Popen([sys.executable, "redditchaosbot.py"]) + # Schedule all cron jobs to be run cron.schedule_jobs(api, api_twitter) - log.info("Setting description to {desc}".format(desc=settings.REPO_DESCRIPTION)) + log.info("Setting description to {desc}".format( + desc=settings.REPO_DESCRIPTION)) github_api.repos.set_desc(api, settings.URN, settings.REPO_DESCRIPTION) log.info("Ensure creation of issue/PR labels") @@ -138,7 +148,8 @@ def check_for_prev_crash(api, log): # Currently, I'm just reading the last 200 lines... which I think # ought to be enough, but if anyone has a better way to do this, # please do it. - dump = subprocess.check_output(["tail", "-n", "200", settings.CHAOSBOT_STDERR_LOG]) + dump = subprocess.check_output( + ["tail", "-n", "200", settings.CHAOSBOT_STDERR_LOG]) # Create a github issue for the problem title = "Help! I crashed! --CB" diff --git a/encrypt_config_file.py b/encrypt_config_file.py new file mode 100644 index 00000000..50f9455d --- /dev/null +++ b/encrypt_config_file.py @@ -0,0 +1,23 @@ +import sys +from cryptography.fernet import Fernet +from symmetric_keys import KeyManager + +# You gotta tell us what to call this key +try: + key_name = sys.argv[1] + file_to_encrypt = sys.argv[2] +except KeyError: + print("Usage:", sys.argv[0], "keyname file_to_encrypt") + sys.exit(1) + +# Make the symmetric key +key = Fernet.generate_key() + +with KeyManager() as key_manager: + key_manager.add_key(key_name, key) + +with open(file_to_encrypt, 'r+b') as config_file: + contents = config_file.read() + config_file.seek(0) + config_file.write(Fernet(key).encrypt(contents)) + config_file.flush() diff --git a/encryption.py b/encryption.py index ef96cba4..e5f4c1ee 100644 --- a/encryption.py +++ b/encryption.py @@ -2,8 +2,10 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa +__all__ = ["encrypt", "decrypt"] -def create_decryptor(private_location, public_location): + +def create_crypto_functions(private_location, public_location): try: with open(private_location, "rb") as key_file: private_key = serialization.load_pem_private_key( @@ -32,6 +34,14 @@ def create_decryptor(private_location, public_location): ) public_file.write(pem) + def encrypt(plaintext): + return public_key.encrypt(plaintext, + padding.OAEP( + padding.MGF1(hashes.SHA1()), + hashes.SHA1(), + None + )) + def decrypt(ciphertext): return private_key.decrypt( ciphertext, @@ -42,7 +52,7 @@ def decrypt(ciphertext): ) ) - return decrypt + return encrypt, decrypt -decrypt = create_decryptor("privkey", "server/pubkey.txt") +encrypt, decrypt = create_crypto_functions("/etc/privkey", "server/pubkey.txt") diff --git a/redditbot.config b/redditbot.config new file mode 100644 index 00000000..d5b6a4e0 --- /dev/null +++ b/redditbot.config @@ -0,0 +1 @@ +gAAAAABZMhR4ejTZdcGI3-W4ANpPk9EfyiQP71Nafrj0YLyFML1yctx_tzEeJcXT9-9Tp51z9hD0aDos7ivXFOlth7sRlDojY6-v-2T3_-wv3_jfTpQNV4zxfkzrKFVDRX4kVcNhu6YkkORAcJZu_5aNgCNBTGh2tQjG4WVIfvvCx-2RyP5TKOFC5CPSMlVvTqeaau4GbK2Qk7ZJq6oMCI00z5jSeRQW2Uh87zKnvu_HZicxDIjTIJfNP0bTxB08eLcjP5hr4cNqDSkc0-JOIvaPB721intWDuoLVLCragfz56utKslMD3gIDeCsevAUov6tN8b0qYYjJctk0PpWn-TXuxDFXZ8-mRswJbfbQZVWJN4bH84NX2oaAUgan6qclkAMu12VovlMX5dJd_LdmcA2Q_sWIsJlJIdrAtVA8Lrvow8RJ13BQi3tjeV_3g6VD5G0PhJ0HrGPCURTczuXYIpkyKfKXBpU0PnePMjKIf4izUeeBprQQk-ozvGRMBMAlldUtZd91wGY1An6ULrY5fPBWSankJTZdUJG9cM309LSCYZaUgLZWMw= \ No newline at end of file diff --git a/redditchaosbot.py b/redditchaosbot.py index 4876561e..a50eec7c 100755 --- a/redditchaosbot.py +++ b/redditchaosbot.py @@ -1,35 +1,87 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - +import logging +import json +import sys +import time +from atomicwrites import atomic_write import praw +from cryptography.fernet import Fernet +from symmetric_keys import KeyManager +from server.server import set_proc_name +set_proc_name("chaos_redditbot") +log = logging.getLogger("chaos_redditbot") +handler = logging.StreamHandler() +formatter = logging.Formatter( + '%(asctime)s %(name)-12s %(levelname)-8s %(message)s') +handler.setFormatter(formatter) +log.addHandler(handler) +log.setLevel(logging.INFO) '''Authenticated instance of Reddit''' # -------------------------------------------------------------------- # https://praw.readthedocs.io/en/latest/getting_started/installation.html # https://www.reddit.com/prefs/apps -reddit = praw.Reddit(user_agent='Chaosbot social experiment', - client_id='KbKcHMguzNRKUw', client_secret="n-2lxQYOKtJinlJUWJr_Zv7g1rw", - username='redditchaosbot', password='cha0sb0t') -reddit.read_only = True +try: + with open("redditbot.config", 'rb') as config_file: + with KeyManager() as key_manager: + key = key_manager.get_key("redditchaosbot") + fernet = Fernet(key) + config = json.loads(fernet.decrypt(config_file.read()).decode('utf-8')) +except FileNotFoundError as ex: + log.critical(ex) + sys.exit(1) -submission = reddit.submission(id='6criij') -subreddit = reddit.subreddit('programming') -# -------------------------------------------------------------------- +reddit = praw.Reddit( + user_agent='Chaosbot social experiment', + client_id=config['client_id'], + client_secret=config['client_secret'], + username='chaosthebotreborn', + password=config['password']) +subreddit = reddit.subreddit('chaosthebot') -'''WHAT ARE PEOPLE SAYING ABOUT CHAOSBOT?''' -# -------------------------------------------------------------------- -# in reddit thread: https://goo.gl/5ETNmF -submission.comment_sort = 'new' -top_level_comments = list(submission.comments) -newcom = top_level_comments[1] -print(newcom.body) +already_replied = set() +replied_file = "redditbot_replied.json" +try: + with open(replied_file) as f: + already_replied.update(json.load(f)['already_replied']) +except: + pass -'''FIND COMMENTS ABOUT CHAOS''' -# -------------------------------------------------------------------- -# TBD -# for comment in reddit.subreddit('programming').comments(limit=5): -# import pdb; pdb.set_trace() + +def save_already_replied(replied_list): + with atomic_write(replied_file, overwrite=True) as f: + json.dump({'already_replied': list(replied_list)}, f) + + +def process_comment(comment): + log.info("Processing comment id %s", comment.id) + if comment.id not in already_replied and "hey chaosbot" in comment.body.lower(): + log.info("Attempting to reply to comment %s", comment.id) + comment.reply("Hey {}!".format(comment.author.name)) + already_replied.add(comment.id) + # Save every chance we get + save_already_replied(already_replied) + + +def main(): + try: + for comment in subreddit.stream.comments(): + try: + process_comment(comment) + except praw.exceptions.APIException: + # Save every chance we get + save_already_replied(already_replied) + sleep_time = reddit.auth.limits['reset_timestamp'] - time.time() + log.info("Sleeping for %d seconds", sleep_time) + time.sleep(sleep_time) + finally: + save_already_replied(already_replied) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index ec09bb5f..014dd914 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,19 +2,29 @@ ansible==2.3.0.0 appdirs==1.4.3 arrow==0.10.0 asn1crypto==0.22.0 +atomicwrites==1.1.5 +certifi==2017.4.17 cffi==1.10.0 +chardet==3.0.3 cryptography==1.9 emoji==0.4.5 enum34==1.1.6 +flake8==3.3.0 idna==2.5 ipaddress==1.0.18 Jinja2==2.9.6 MarkupSafe==1.0 +mccabe==0.6.1 packaging==16.8 paramiko==2.1.2 +praw==4.5.1 +prawcore==0.10.1 pyasn1==0.2.3 +pycodestyle==2.3.1 pycparser==2.17 pycrypto==2.6.1 +pyflakes==1.5.0 +PyMySQL==0.7.11 pyparsing==2.2.0 python-dateutil==2.6.0 PyYAML==3.12 @@ -22,10 +32,11 @@ requests==2.17.3 schedule==0.4.2 sh==1.12.13 six==1.10.0 -flake8==3.3.0 unidiff==0.5.4 +update-checker==0.16 +urllib3==1.21.1 python-twitter==3.3 pymysql hug==2.3.0 uWSGI==2.0.15 -peewee \ No newline at end of file +peewee diff --git a/symmetric_keys.json b/symmetric_keys.json new file mode 100644 index 00000000..faba373d --- /dev/null +++ b/symmetric_keys.json @@ -0,0 +1 @@ +{"redditchaosbot": "XZNtqEBHjid0OdZ+jlgV6fUnuVBH9OjAVpM1KCJ8/WXEniFLilENgji2KfS+ZRVLevojcimhR2vMYPhCTi8ogZw4aqenvb1s+oDmSJ1F+wk8P8B3wdULoamg5ccM+4xANQyptsV5WJSqR1hfqoAMYIsXSR4O/gbArl9ga0dkQJRGs7aQvMqz22bSw+LI3fnuhyqi5fT0KZmCrotMptig/ocQcBuZoVraWAfrTyLzCKEp0aAHmRncdOAr2pqRCwNpcCNV19ScofYEcakLav5vA7XOSVIx9hRWQbBNfJPCSCZBy69VqGU8N+Jf9IILyCh3Q1HgiN7Ho1kY5roA344gAg=="} \ No newline at end of file diff --git a/symmetric_keys.py b/symmetric_keys.py new file mode 100644 index 00000000..9072332c --- /dev/null +++ b/symmetric_keys.py @@ -0,0 +1,59 @@ +import json +import base64 +try: + from encryption import decrypt, encrypt +except: + # We may need to encrypt when not in a server environment + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + from cryptography.hazmat.primitives.serialization import load_pem_public_key + + def encrypt(plaintext): + with open("server/pubkey.txt", "rb") as key_file: + public_key = load_pem_public_key(key_file.read(), default_backend()) + return public_key.encrypt(plaintext, + padding.OAEP( + padding.MGF1(hashes.SHA1()), + hashes.SHA1(), + None + )) + + def decrypt(ciphertext): + raise NotImplementedError("This only works in a running server env") + + +# It's a context manager y'all +class KeyManager(): + + def __init__(self, keys_file_name="symmetric_keys.json"): + self.keys_file_name = keys_file_name + self.existing_keys = {} + + # Try to load existing keys + try: + with open(keys_file_name) as keys_file: + self.existing_keys = json.load(keys_file) + except FileNotFoundError: + # Welp, aren't any + pass + + def __enter__(self): + return self + + def get_key(self, key_name): + # base64 decode the string object in the dict and decrypt and return + # base64.b64decode() returns bytes for us + return decrypt(base64.b64decode(self.existing_keys[key_name])) + + def add_key(self, key_name, key): + # encrypt the base64 encoded ASCII bytes then b64encode that + # and decode it to a Unicode string (JSON doesn't work with bytes) + # and store it + self.existing_keys[key_name] = base64.b64encode( + encrypt(key)).decode('ascii') + + def __exit__(self, exc_type, exc_value, traceback): + # Save the keys when we are finished + with open(self.keys_file_name, 'w') as keys_file: + json.dump(self.existing_keys, keys_file)