├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── common_settings.py ├── docker_settings.py ├── envlist-sample ├── goodreadsapi.py ├── main.py ├── requirements.txt ├── sample_settings.py ├── supervisor.conf └── welcome_messages.json /.dockerignore: -------------------------------------------------------------------------------- 1 | settings_sample.py 2 | settings.py 3 | envlist 4 | envlist-sample 5 | .git 6 | .gitignore 7 | LICENSE 8 | README.md 9 | supervisor.conf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # OS Files 39 | .DS_Store 40 | 41 | # virtualenv 42 | venv/ 43 | 44 | # tokens 45 | settings.py 46 | envlist 47 | 48 | # db for bot 49 | goodreadsbot.db 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-slim 2 | 3 | RUN apt-get update && apt-get install -y pandoc 4 | 5 | ADD . /home/ubuntu/bot/ 6 | 7 | WORKDIR /home/ubuntu/bot/ 8 | 9 | RUN pip install -r requirements.txt 10 | 11 | RUN mv docker_settings.py settings.py 12 | 13 | ENTRYPOINT python main.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Avinash Sajjanshetty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goodreads Bot for Reddit! 2 | 3 | ## Requirements: 4 | 5 | #### System Requirements: 6 | 7 | - Python 3.6+ 8 | - Requires [Pandoc](http://pandoc.org/): 9 | 10 | - OS X: `brew install pandoc` 11 | - Ubuntu/Debian: `sudo apt-get install pandoc` 12 | 13 | #### Python Requirements: 14 | `pip install -r requirements.txt` 15 | 16 | ### Docker 17 | 18 | - Rename `envlist-sample` to `envlist` 19 | - Build docker image: `docker build -t reddit-goodreads .` 20 | - Run: `docker run --mount source=reddit-goodreads-volume,target=/home/ubuntu/db --env-file ./envlist --rm -it reddit-goodreads:latest` 21 | 22 | 23 | ## Todo: 24 | - ~~Add Peewee Support~~ 25 | - ~~Log all successful comments~~ 26 | - ~~Use Oauth~~ 27 | - ~~Handle HTTP Exceptions (`requests`) and log it~~ (not needed) 28 | - Log all fails, exceptions 29 | - ~~Custom reply to those who reply to bot~~ 30 | - ~~Remove HTTP tags in the response. Better, change
to \n and rest all to markdown conversion~~ 31 | 32 | ## LICENSE 33 | 34 | The mighty MIT License. Check `LICENSE` file. 35 | -------------------------------------------------------------------------------- /common_settings.py: -------------------------------------------------------------------------------- 1 | # Common, non secret settings 2 | # Used in both for Docker and non-Docker deployments 3 | # Check `settings_sample.py` or `docker_settings.py` 4 | 5 | supported_subreddits = 'india+indianbooks+52in52+indianreaders' 6 | user_agent = ('Goodreads, v0.1. Gives info of the book whenever goodreads' 7 | 'link to a book is posted. (by /u/avinassh)') 8 | -------------------------------------------------------------------------------- /docker_settings.py: -------------------------------------------------------------------------------- 1 | # this file will be used by Docker where secrets are from ENV 2 | # Check Dockerfile 3 | # Rename this file to `settings.py` in deployment 4 | 5 | import os 6 | 7 | from common_settings import * 8 | 9 | # reddit app 10 | app_key = os.getenv("REDDIT_APP_KEY") 11 | app_secret = os.getenv("REDDIT_APP_SECRET") 12 | 13 | # bot account 14 | username = os.getenv("BOT_USERNAME") 15 | password = os.getenv("BOT_PASSWORD") 16 | 17 | # good reads 18 | goodreads_api_key = os.getenv("GOODREADS_API_KEY") 19 | goodreads_api_secret = os.getenv("GOODREADS_API_SECRET") 20 | -------------------------------------------------------------------------------- /envlist-sample: -------------------------------------------------------------------------------- 1 | REDDIT_APP_KEY=R...w 2 | REDDIT_APP_SECRET=w...g 3 | GOODREADS_API_KEY=5...Q 4 | GOODREADS_API_SECRET=Y...4 5 | BOT_USERNAME=goodreadsbot 6 | BOT_PASSWORD=H...8 7 | DB_LOCATION=/home/ubuntu/db/goodreadsbot.db 8 | -------------------------------------------------------------------------------- /goodreadsapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | from xml.parsers.expat import ExpatError 5 | 6 | import requests 7 | import xmltodict 8 | 9 | from settings import goodreads_api_key 10 | 11 | 12 | def get_goodreads_ids(comment_msg): 13 | # receives goodreads url 14 | # returns the id using regex 15 | regex = r'goodreads.com/book/show/(\d+)' 16 | return set(re.findall(regex, comment_msg)) 17 | 18 | 19 | def get_book_details_by_id(goodreads_id): 20 | api_url = 'http://goodreads.com/book/show/{0}?format=xml&key={1}' 21 | r = requests.get(api_url.format(goodreads_id, goodreads_api_key)) 22 | try: 23 | book_data = xmltodict.parse(r.content)['GoodreadsResponse']['book'] 24 | except (TypeError, KeyError, ExpatError): 25 | return False 26 | keys = ['title', 'average_rating', 'ratings_count', 'description', 27 | 'num_pages'] 28 | book = {} 29 | for k in keys: 30 | book[k] = book_data.get(k) 31 | try: 32 | work = book_data['work'] 33 | book['publication_year'] = work['original_publication_year']['#text'] 34 | except KeyError: 35 | book['publication_year'] = book_data.get('publication_year') 36 | 37 | if type(book_data['authors']['author']) == list: 38 | authors = [author['name'] for author in book_data['authors']['author']] 39 | authors = ', '.join(authors) 40 | else: 41 | authors = book_data['authors']['author']['name'] 42 | book['authors'] = authors 43 | return book 44 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | import json 5 | import random 6 | import os 7 | 8 | import praw 9 | import prawcore 10 | from peewee import (SqliteDatabase, Model, CharField, OperationalError, 11 | DoesNotExist) 12 | import pypandoc 13 | 14 | from goodreadsapi import get_book_details_by_id, get_goodreads_ids 15 | from settings import (app_key, app_secret, username, password, 16 | user_agent, supported_subreddits) 17 | 18 | # instantiate goodreads and reddit clients 19 | reddit_client = praw.Reddit(user_agent=user_agent, client_id=app_key, 20 | client_secret=app_secret, username=username, 21 | password=password) 22 | 23 | 24 | replied_comments = [] 25 | last_checked_comment = [] 26 | thanked_comments = [] 27 | db = SqliteDatabase(os.getenv('DB_LOCATION', 'goodreadsbot.db')) 28 | 29 | 30 | with open('welcome_messages.json') as f: 31 | welcome_messages = json.load(f)['messages'] 32 | 33 | 34 | class RepliedComments(Model): 35 | comment_id = CharField() 36 | author = CharField() 37 | subreddit = CharField() 38 | 39 | class Meta: 40 | database = db 41 | 42 | 43 | class ThankedComments(Model): 44 | comment_id = CharField() 45 | author = CharField() 46 | subreddit = CharField() 47 | 48 | class Meta: 49 | database = db 50 | 51 | 52 | def initialize_db(): 53 | db.connect() 54 | try: 55 | db.create_tables([RepliedComments, ThankedComments]) 56 | except OperationalError: 57 | # Table already exists. Do nothing 58 | pass 59 | 60 | 61 | def deinit(): 62 | db.close() 63 | 64 | 65 | def is_already_replied(comment_id): 66 | if comment_id in replied_comments: 67 | return True 68 | try: 69 | RepliedComments.select().where( 70 | RepliedComments.comment_id == comment_id).get() 71 | return True 72 | except DoesNotExist: 73 | return False 74 | 75 | 76 | def is_already_thanked(comment_id): 77 | if comment_id in thanked_comments: 78 | return True 79 | 80 | 81 | def log_this_comment(comment, TableName=RepliedComments): 82 | comment_data = TableName(comment_id=comment.id, 83 | author=comment.author.name, 84 | subreddit=comment.subreddit.display_name) 85 | comment_data.save() 86 | replied_comments.append(comment.id) 87 | 88 | 89 | def get_a_random_message(): 90 | return random.choice(welcome_messages) 91 | 92 | 93 | def get_latest_comments(subreddit): 94 | subreddit = reddit_client.subreddit(subreddit) 95 | return subreddit.comments() 96 | 97 | 98 | def prepare_the_message(spool): 99 | message_template = ("**Name**: {0}\n\n**Author**: {1}\n\n**Avg Rating**: " 100 | "{2} by {3} users\n\n**Description**: {4}\n\n Pages: " 101 | "{5}, Year: {6}") 102 | message = "" 103 | for book in spool: 104 | message += message_template.format(book['title'], 105 | book['authors'], 106 | book['average_rating'], 107 | book['ratings_count'], 108 | html_to_md(book['description']), 109 | book['num_pages'], 110 | book['publication_year']) 111 | message += '\n\n---\n\n' 112 | message += ('^(Bleep, Blop, Bleep! I am still in beta, please be nice. ' 113 | 'Contact )[^(my creator)](https://www.reddit.com/message/' 114 | 'compose/?to=avinassh) ^(for feedback, bug reports or just to ' 115 | 'say thanks! The code is on )[^github](https://github.com/' 116 | 'avinassh/Reddit-GoodReads-Bot)^.') 117 | return message 118 | 119 | 120 | def html_to_md(string): 121 | # remove the
tags before conversion 122 | if not string: 123 | return 124 | string = string.replace('
', ' ') 125 | return pypandoc.convert(string, 'md', format='html') 126 | 127 | 128 | def take_a_nap(): 129 | time.sleep(30) 130 | 131 | 132 | def goodreads_bot_serve_people(subreddit='india'): 133 | global last_checked_comment 134 | for comment in get_latest_comments(subreddit): 135 | if comment.id in last_checked_comment: 136 | break 137 | last_checked_comment.append(comment.id) 138 | if 'goodreads.com' not in comment.body: 139 | continue 140 | author = comment.author 141 | if author.name == 'goodreadsbot': 142 | continue 143 | if is_already_replied(comment.id): 144 | break 145 | goodread_ids = get_goodreads_ids(comment.body) 146 | if not goodread_ids: 147 | continue 148 | spool = map(get_book_details_by_id, goodread_ids) 149 | message = prepare_the_message(spool) 150 | 151 | if len(message) > 9999: 152 | error = ('You have linked to many books in your comment and ' 153 | 'my response crossed Reddit\'s 10k limit. Sorry!') 154 | comment.reply(error) 155 | log_this_comment(comment) 156 | replied_comments.append(comment.id) 157 | continue 158 | 159 | comment.reply(message) 160 | log_this_comment(comment) 161 | replied_comments.append(comment.id) 162 | 163 | 164 | def reply_to_self_comments(): 165 | for comment in reddit_client.inbox.comment_replies(): 166 | if is_already_thanked(comment_id=comment.id) or not comment.new: 167 | break 168 | comment.mark_read() 169 | if 'thank' in comment.body.lower(): 170 | comment.reply(get_a_random_message()) 171 | thanked_comments.append(comment.id) 172 | log_this_comment(comment, TableName=ThankedComments) 173 | 174 | 175 | def main(): 176 | while True: 177 | try: 178 | reply_to_self_comments() 179 | goodreads_bot_serve_people(subreddit=supported_subreddits) 180 | except prawcore.exceptions.RequestException: 181 | pass 182 | take_a_nap() 183 | 184 | 185 | if __name__ == '__main__': 186 | initialize_db() 187 | main() 188 | deinit() 189 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.3.2 2 | requests==2.7.0 3 | xmltodict==0.9.2 4 | peewee==2.6.1 5 | praw==5.2.0 6 | pypandoc==1.4 7 | -------------------------------------------------------------------------------- /sample_settings.py: -------------------------------------------------------------------------------- 1 | # Rename this file to `settings.py` in deployment 2 | 3 | from common_settings import * 4 | 5 | # reddit app 6 | app_key = 'K...q' 7 | app_secret = 'y...i' 8 | 9 | # bot account 10 | access_token = '3...R' 11 | refresh_token = '3...m' 12 | 13 | # good reads 14 | goodreads_api_key = '5...v' 15 | goodreads_api_secret = 'T...4' 16 | -------------------------------------------------------------------------------- /supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:goodreads_reddit_bot] 2 | command=/usr/bin/python3 /opt/goodreads_bot/main.py 3 | directory=/opt/goodreads_bot/ 4 | stdout_logfile=/opt/goodreads_bot/log/stdout.log 5 | stderr_logfile=/opt/goodreads_bot/log/stderr.log 6 | redirect_stderr=true 7 | stdout_logfile_maxbytes=1MB 8 | stdout_logfile_backups=10 9 | stderr_logfile_maxbytes=1MB 10 | autostart=true 11 | autorestart=true 12 | -------------------------------------------------------------------------------- /welcome_messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": [ 3 | "Hey! You are welcome :)", 4 | "I am pleased to serve you, sire", 5 | "Welcome mate, even I like reading books", 6 | "Well, I am a bot and I am just doing my work yo!", 7 | "You can message my master and thank him too!", 8 | "I am happy to make you happy", 9 | "Great! Please give some feedback, so that my master can improve me", 10 | "Welcome! Would you like to be a part of my robot colony?", 11 | "You are welcome! As a tip, I suggest you to read The Hitchhiker's Guide to the Galaxy series by Douglas Adams. You will love my friend Marvin", 12 | "Welcome :) (self note: Humans are so easy to fool *evil smile*)", 13 | "Pass me the puff my homie", 14 | "Send me some weed instead brah", 15 | "Fermina I have waited for this opportunity for 51 years, nine months and four days. That is... how long I have loved you from the first moment I cast eyes on you un... until now.", 16 | "Welcome :) *sends OP's book reading habits to NSA*", 17 | "Since you are nice to me, I will tell you a secret: Be ready for robot apocalypse" 18 | ] 19 | } --------------------------------------------------------------------------------