├── .gitignore ├── requirements.txt ├── resources └── masks │ ├── alien.png │ └── heart.png ├── README.md └── common ├── config.py ├── cloud.py ├── __main__.py └── reddit.py /.gitignore: -------------------------------------------------------------------------------- 1 | praw.ini 2 | *.json 3 | *.pyc 4 | *.ttf 5 | *.otf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | image 2 | numpy 3 | pyimgur 4 | praw 5 | wordcloud -------------------------------------------------------------------------------- /resources/masks/alien.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/win0na/makeswordclouds/HEAD/resources/masks/alien.png -------------------------------------------------------------------------------- /resources/masks/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/win0na/makeswordclouds/HEAD/resources/masks/heart.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | makeswordclouds 2 | --------------- 3 | 4 | A word cloud bot, currently running under /u/makeswordcloudsagain. The current version is v2.0.1. -------------------------------------------------------------------------------- /common/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | from os import path 5 | 6 | class Config: 7 | def __init__(self, name, template): 8 | self.config_path = path.dirname(path.dirname(__file__)) + name 9 | 10 | if not path.isfile(self.config_path): 11 | self._write(self.config_path, template) 12 | 13 | self.config = json.loads(open(self.config_path).read()) 14 | 15 | def _write(self, filepath, template): 16 | with open(filepath, "w") as w: 17 | w.write(json.dumps(template, indent=4, sort_keys=True)) 18 | 19 | def get(self): 20 | return self.config 21 | 22 | def save(self): 23 | self._write(self.config_path, self.config) -------------------------------------------------------------------------------- /common/cloud.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | import numpy 5 | import pyimgur 6 | 7 | from PIL import Image 8 | from wordcloud import WordCloud, ImageColorGenerator 9 | 10 | """ 11 | 12 | mask = numpy.array(Image.open(path.join(path.dirname(path.dirname(__file__)), "resources") + "/earth.png")) 13 | cloud = WordCloud(font_path=path.join(path.dirname(path.dirname(__file__)), "resources") + "/fonts/quartzo.ttf", background_color="#1A1A1A", mask=mask, scale=2, max_words=None, relative_scaling=0.5, prefer_horizontal=1.0) 14 | 15 | cloud.generate(r.flatten("4d1yv4")) 16 | 17 | image_colors = ImageColorGenerator(mask) 18 | cloud.recolor(color_func=image_colors) 19 | 20 | cloud.to_file("cloud.png") 21 | 22 | """ 23 | 24 | def generate(text): 25 | resources = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources") 26 | masks = os.path.join(resources, "masks") 27 | fonts = os.path.join(resources, "fonts") 28 | 29 | mask = numpy.array(Image.open(os.path.join(masks, random.choice(os.listdir(masks))))) 30 | 31 | cloud = WordCloud( 32 | font_path=os.path.join(fonts, random.choice(os.listdir(fonts))), 33 | background_color="#1A1A1A", 34 | mask=mask, 35 | scale=2, 36 | max_words=None, 37 | relative_scaling=0.5, 38 | prefer_horizontal=1.0 39 | ) 40 | 41 | cloud.generate(text) 42 | 43 | image_colors = ImageColorGenerator(mask) 44 | cloud.recolor(color_func=image_colors) 45 | 46 | cloud.to_file("cloud.png") 47 | 48 | def upload(client): 49 | imgur = pyimgur.Imgur(client) 50 | upload = imgur.upload_image("cloud.png") 51 | os.remove("cloud.png") 52 | 53 | return upload.link -------------------------------------------------------------------------------- /common/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import traceback 5 | 6 | import praw 7 | 8 | import config 9 | import reddit 10 | import cloud 11 | 12 | class Main: 13 | def __init__(self): 14 | self.version = "2.0.1" 15 | 16 | self.c = config.Config("config.json", 17 | { 18 | "id": "", 19 | "min": 100 20 | } 21 | ) 22 | 23 | self.d = config.Config("database.json", 24 | { 25 | "banned": [], 26 | "replied": [] 27 | } 28 | ) 29 | 30 | print "> Started makeswordclouds, version " + self.version 31 | self.r = reddit.Reddit(self.c, self.d, praw.Reddit("makeswordclouds: running under version " + self.version)) 32 | 33 | def legwork(self, submission, requester=None): 34 | prepend = "" 35 | text = self.r.flatten(submission) 36 | 37 | cloud.generate(text) 38 | upload = cloud.upload(self.c.get()["id"]) 39 | 40 | if requester != None: 41 | prepend = "**Summoned by /u/" + requester + ".** \n" 42 | 43 | content = ( 44 | prepend + 45 | "Here is a word cloud of every comment in this thread, as of this time: " + upload 46 | ) 47 | 48 | return self.r.comment(submission, content) 49 | 50 | def loop(self): 51 | try: 52 | while True: 53 | print "> Beginning mailbox check." 54 | 55 | for mail in self.r.mailbox(): 56 | try: 57 | url = self.legwork(mail.submission.id, requester=mail.message.author.name) 58 | 59 | self.r.message( 60 | mail.message.author.name, 61 | "Your word cloud has been created.", 62 | "Congratulations! Your word cloud can be found here: " + url 63 | ) 64 | except reddit.BannedSubredditError as e: 65 | try: 66 | raise reddit.BannedSubredditError( 67 | resp=reddit.ErrorResponse(self.r, mail.message.author.name), 68 | log=False 69 | ) 70 | except: 71 | pass 72 | except Exception as e: 73 | self.r.message( 74 | mail.message.author.name, 75 | "A problem arose creating your word cloud.", 76 | "An internal error occured. I am unable to furthur explain the issue. Contact the developer if you want to know the full cause." 77 | ) 78 | 79 | print "> An error occrred while creating the word cloud." 80 | 81 | print "> Beginning post check." 82 | 83 | for post in self.r.posts(): 84 | try: 85 | self.legwork(post) 86 | except reddit.BannedSubredditError: 87 | pass 88 | 89 | print "> Sleeping." 90 | 91 | self.d.save() 92 | time.sleep(120) 93 | except KeyboardInterrupt: 94 | print "> Terminated." 95 | self.d.save() 96 | except: 97 | print "> An error occured. Restarting." 98 | 99 | traceback.print_exc(file=sys.stdout) 100 | self.loop() 101 | 102 | if __name__ == "__main__": 103 | Main().loop() -------------------------------------------------------------------------------- /common/reddit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import traceback 4 | 5 | import praw 6 | 7 | class ErrorResponse: 8 | def __init__(self, reddit, recipient): 9 | self.reddit = reddit 10 | self.recipient = recipient 11 | 12 | class MakesWordCloudsErrorDummy(Exception): 13 | def __init__(self, resp, message, log): 14 | Exception.__init__(self, message) 15 | 16 | if resp == None: 17 | self.makeswordclouds_cont = True 18 | else: 19 | resp.reddit.message( 20 | resp.recipient, 21 | "A problem arose creating your word cloud.", 22 | message 23 | ) 24 | 25 | if log: 26 | print "> " + message 27 | 28 | class InvalidPermalinkError(MakesWordCloudsErrorDummy): 29 | def __init__(self, resp=None, message="The permalink is invalid.", log=True): 30 | MakesWordCloudsErrorDummy.__init__(self, resp, message, log) 31 | 32 | class PreexistingCloudError(MakesWordCloudsErrorDummy): 33 | def __init__(self, resp=None, message="The permalink already has a word cloud in its comments.", log=True): 34 | MakesWordCloudsErrorDummy.__init__(self, resp, message, log) 35 | 36 | class BannedSubredditError(MakesWordCloudsErrorDummy): 37 | def __init__(self, resp=None, message="The subreddit is on the blacklist.", log=True): 38 | MakesWordCloudsErrorDummy.__init__(self, resp, message, log) 39 | 40 | class ValidMail: 41 | def __init__(self, submission, message): 42 | self.submission = submission 43 | self.message = message 44 | 45 | class Reddit: 46 | def __init__(self, config, database, reddit): 47 | self.config = config 48 | self.database = database 49 | 50 | self.reddit = reddit 51 | self.reddit.refresh_access_information() 52 | 53 | self.footer = ( 54 | "---\n\n" 55 | "[^[source ^code]](https://github.com/Winneon/makeswordclouds) " 56 | "[^[contact ^developer]](https://reddit.com/user/WinneonSword) " 57 | "[^[request ^word ^cloud]](https://www.reddit.com/message/compose/?to=makeswordcloudsagain&subject=Requesting%20word%20cloud.&message=%2Bcreate%20REPLACE_THIS_WITH_A_REDDIT_POST_PERMALINK)" 58 | ) 59 | 60 | print "> Authenticated as /u/" + self.reddit.get_me().name 61 | 62 | def _format_comment(self, text): 63 | return text + "\n\n" + self.footer 64 | 65 | def flatten(self, submission): 66 | submission = self.reddit.get_submission(submission_id=submission, comment_limit=None) 67 | flattened = praw.helpers.flatten_tree(submission.comments) 68 | text_mass = "" 69 | 70 | for comment in flattened: 71 | if isinstance(comment, praw.objects.Comment): 72 | # i hate these 3 lines of code but i'm too lazy to redo them 73 | body = re.sub(r"https?://(?:www\.)?[A-z0-9-]+\.[A-z\.]+[\?A-z0-9&=/]*", "", comment.body, flags=re.IGNORECASE) 74 | body = re.sub(r"<.*?>|&.*?;|/.+?(?= )|/.*", "", body) 75 | text_mass = text_mass + body + "\n" 76 | 77 | return text_mass 78 | 79 | def mailbox(self): 80 | collected = [] 81 | 82 | for message in self.reddit.get_unread(limit=None): 83 | if "+create " in message.body: 84 | submission = None 85 | 86 | try: 87 | permalink = message.body.replace("+create ", "") 88 | 89 | if "redd.it" in permalink: 90 | short = permalink.replace("http://redd.it/", "").replace("https://redd.it/", "") 91 | submission = self.reddit.get_submission(submission_id=short) 92 | else: 93 | submission = self.reddit.get_submission(url=permalink) 94 | except: 95 | pass 96 | 97 | try: 98 | if submission == None: 99 | raise InvalidPermalinkError(resp=ErrorResponse(self, message.author.name)) 100 | elif submission.id in self.database.get()["replied"]: 101 | raise PreexistingCloudError(resp=ErrorResponse(self, message.author.name)) 102 | elif submission.subreddit.display_name.lower() in self.database.get()["banned"]: 103 | raise BannedSubredditError(resp=ErrorResponse(self, message.author.name)) 104 | else: 105 | collected.append(ValidMail(submission, message)) 106 | except MakesWordCloudsErrorDummy: 107 | pass 108 | 109 | message.mark_as_read() 110 | 111 | return collected 112 | 113 | def posts(self): 114 | collected = [] 115 | 116 | for post in self.reddit.get_subreddit("all").get_hot(limit=200): 117 | subreddit = post.subreddit.display_name 118 | limit = int(self.config.get()["min"]) 119 | 120 | banned = self.database.get()["banned"] 121 | replied = self.database.get()["replied"] 122 | 123 | if subreddit.lower() not in banned and post.id not in replied and post.num_comments >= limit: 124 | collected.append(post.id) 125 | 126 | return collected 127 | 128 | def comment(self, submission, message): 129 | submission = self.reddit.get_submission(submission_id=submission) 130 | 131 | try: 132 | url = submission.add_comment(self._format_comment(message)).permalink 133 | self.database.get()["replied"].append(submission.id) 134 | 135 | print "> Comment posted. " 136 | print "> " + url 137 | 138 | return url 139 | except praw.errors.Forbidden as e: 140 | subreddit = submission.subreddit.display_name.lower() 141 | banned = self.database.get()["banned"] 142 | 143 | if subreddit not in banned: 144 | banned.append(subreddit) 145 | raise BannedSubredditError() 146 | except: 147 | print "> Failed to post comment." 148 | 149 | self.database.get()["replied"].append(submission.id) 150 | traceback.print_exc(file=sys.stdout) 151 | 152 | def message(self, recipient, subject, message): 153 | self.reddit.send_message(recipient, subject, self._format_comment(message)) --------------------------------------------------------------------------------