Changed config file format to TOML

This commit is contained in:
Collin R
2020-02-03 18:53:23 -08:00
parent 5b3a3d8f1f
commit 107e5f6bfb
6 changed files with 167 additions and 88 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
praw.ini praw.ini
pointsbot.ini pointsbot.ini
pointsbot.toml
*.db *.db
*.pyc *.pyc

72
pointsbot.sample.toml Normal file
View File

@@ -0,0 +1,72 @@
################################################################################
# Core
#
# These are the primary fields needed to run the bot.
################################################################################
[core]
# The name of the subreddit to monitor, without the "r/" prefix.
subreddit = ""
################################################################################
# Filepaths
#
# Any filepaths needed by the bot. Can be relative or absolute paths, or just
# filenames.
################################################################################
[filepaths]
# The name of the SQLite database file to use.
database = "pointsbot.db"
################################################################################
# Reddit Credentials
#
# See the bot documentation for more information about these fields.
# Some of these fields are sensitive, so once these fields are provided, take
# care not to upload or distribute this file through unsecure channels, or to
# public sites.
################################################################################
[credentials]
client_id = ""
client_secret = ""
username = ""
password = ""
################################################################################
# User Levels
#
# These items represent the levels that contributors to the subreddit can
# attain. To add a level, add another section with the format:
#
# [[levels]]
# name = "<string>"
# points = <integer>
# flair_template_id = "<string>"
#
# These fields are case-sensitive, so enter the level name exactly as you want
# it to appear.
#
# The flair_template_id field is optional, so it may be omitted or left as the
# empty string, "".
################################################################################
[[levels]]
name = "Level One"
points = 1
flair_template_id = ""
[[levels]]
name = "Level Two"
points = 25
flair_template_id = ""
[[levels]]
name = "Level Three"
points = 100
flair_template_id = ""

View File

@@ -4,16 +4,25 @@ import praw
from . import config, database, level, reply from . import config, database, level, reply
### Globals ###
USER_AGENT = 'PointsBot (by u/GlipGlorp7)'
TEST_COMMENTS = False
### Main Function ### ### Main Function ###
def run(): def run():
cfg = config.load() cfg = config.Config.load()
levels = cfg.levels levels = cfg.levels
# Connect to Reddit reddit = praw.Reddit(client_id=cfg.client_id,
reddit = praw.Reddit(site_name=cfg.praw_site_name) client_secret=cfg.client_secret,
subreddit = reddit.subreddit(cfg.subreddit_name) username=cfg.username,
password=cfg.password,
user_agent=USER_AGENT)
subreddit = reddit.subreddit(cfg.subreddit)
print_level(0, f'Connected to Reddit as {reddit.user.me()}') print_level(0, f'Connected to Reddit as {reddit.user.me()}')
print_level(1, f'Read-only? {reddit.read_only}') print_level(1, f'Read-only? {reddit.read_only}')
@@ -21,6 +30,10 @@ def run():
is_mod = bool(subreddit.moderator(redditor=reddit.user.me())) is_mod = bool(subreddit.moderator(redditor=reddit.user.me()))
print_level(1, f'Is mod? {is_mod}') print_level(1, f'Is mod? {is_mod}')
if TEST_COMMENTS:
make_comments(subreddit, levels)
return
db = database.Database(cfg.database_path) db = database.Database(cfg.database_path)
# The pattern that determines whether a post is marked as solved # The pattern that determines whether a post is marked as solved
@@ -28,8 +41,8 @@ def run():
solved_pat = re.compile('![Ss]olved') solved_pat = re.compile('![Ss]olved')
# Monitor new comments for confirmed solutions # Monitor new comments for confirmed solutions
# Passing pause_after=0 will bypass the internal exponential delay, but have # Passing pause_after=0 will bypass the internal exponential delay; instead,
# to check if any comments are returned with each query # have to check if any comments are returned with each query
for comm in subreddit.stream.comments(skip_existing=True, pause_after=0): for comm in subreddit.stream.comments(skip_existing=True, pause_after=0):
if comm is None: if comm is None:
continue continue
@@ -54,24 +67,22 @@ def run():
level_info = level.user_level_info(points, levels) level_info = level.user_level_info(points, levels)
# TODO move comment to the end and use some things, e.g. whether
# flair is set, when building comment (to avoid duplicate logic)
# Reply to the comment marking the submission as solved # Reply to the comment marking the submission as solved
reply_body = reply.make(solver, points, level_info) reply_body = reply.make(solver, points, level_info)
# reply_body = reply.make(solver, points, levels)
print_level(1, f'Replying with: "{reply_body}"') print_level(1, f'Replying with: "{reply_body}"')
comm.reply(reply_body) comm.reply(reply_body)
# Check if (non-mod) user flair should be updated to new level # Check if (non-mod) user flair should be updated to new level
if level_info.current and level_info.current.points == points: lvl = level_info.current
print_level(1, f'User reached level: {level_info.current.name}') if lvl and lvl.points == points:
print_level(1, f'User reached level: {lvl.name}')
if not subreddit.moderator(redditor=solver): if not subreddit.moderator(redditor=solver):
print_level(2, 'Setting flair') print_level(2, 'Setting flair')
print_level(3, f'Flair text: {level_info.current.name}') print_level(3, f'Flair text: {lvl.name}')
# TODO print_level(3, f'Flair template ID: {lvl.flair_template_id}')
# print_level(3, f'Flair template ID: {}') subreddit.flair.set(solver,
subreddit.flair.set(solver, text=levelname) text=lvl.name,
flair_template_id=lvl.flair_template_id)
else: else:
print_level(2, 'Solver is mod; don\'t alter flair') print_level(2, 'Solver is mod; don\'t alter flair')
else: else:
@@ -108,16 +119,6 @@ def is_first_solution(solved_comment, solved_pattern):
return True return True
def find_solver(comment):
# TODO
pass
def make_flair(points, levels):
# TODO
pass
### Debugging & Logging ### ### Debugging & Logging ###
@@ -135,20 +136,7 @@ def print_solution_info(comm):
print_level(3, f'Body: {comm.body}') print_level(3, f'Body: {comm.body}')
def make_comments(): def make_comments(subreddit, levels):
cfg = config.load()
levels = cfg.levels
# Connect to Reddit
reddit = praw.Reddit(site_name=cfg.praw_site_name)
subreddit = reddit.subreddit(cfg.subreddit_name)
print_level(0, f'Connected to Reddit as {reddit.user.me()}')
print_level(1, f'Read-only? {reddit.read_only}')
print_level(0, f'Watching subreddit {subreddit.title}')
is_mod = bool(subreddit.moderator(redditor=reddit.user.me()))
print_level(1, f'Is mod? {is_mod}')
testpoints = [1, 3, 5, 10, 15, 30, 45, 75] + list(range(100, 551, 50)) testpoints = [1, 3, 5, 10, 15, 30, 45, 75] + list(range(100, 551, 50))
for sub in subreddit.new(): for sub in subreddit.new():

View File

@@ -1,53 +1,59 @@
import configparser
import os.path import os.path
# from os.path import abspath, dirname, join
import toml
from .level import Level from .level import Level
### Globals ### ### Globals ###
ROOTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ROOTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
CONFIGPATH = os.path.join(ROOTPATH, 'pointsbot.ini') CONFIGPATH = os.path.join(ROOTPATH, 'pointsbot.toml')
# TODO add default config values to pass to configparser
### Classes ### ### Classes ###
class Config: class Config:
def __init__(self, praw_site_name='', subreddit_name='', database_path='', # Default config vals
levels=None): DEFAULT_DBPATH = 'pointsbot.db'
self.praw_site_name = praw_site_name
self.subreddit_name = subreddit_name def __init__(self, subreddit, client_id, client_secret, username, password,
levels, database_path=DEFAULT_DBPATH):
self.subreddit = subreddit
self.database_path = database_path self.database_path = database_path
self.levels = levels if levels is not None else []
self.client_id = client_id
self.client_secret = client_secret
self.username = username
self.password = password
self.levels = levels
@classmethod @classmethod
def from_configparser(cls, config): def load(cls, filepath=CONFIGPATH):
database_path = os.path.join(ROOTPATH, config['Core']['database_name']) obj = toml.load(filepath)
# Get the user flair levels in ascending order by point value
levels = [] levels = []
for opt in config.options('Levels'): for lvl in obj['levels']:
name, points = opt.title(), config.getint('Levels', opt) flair_template_id = lvl['flair_template_id']
levels.append(Level(name, points)) if flair_template_id == "":
levels.sort(key=lambda lvl: lvl.points) flair_template_id = None
levels.append(Level(lvl['name'], lvl['points'], flair_template_id))
levels.sort(key=lambda l: l.points)
database_path = os.path.join(ROOTPATH, obj['filepaths']['database'])
return cls( return cls(
praw_site_name=config['Core']['praw_site_name'], obj['core']['subreddit'],
subreddit_name=config['Core']['subreddit_name'], obj['credentials']['client_id'],
obj['credentials']['client_secret'],
obj['credentials']['username'],
obj['credentials']['password'],
levels,
database_path=database_path, database_path=database_path,
levels=levels,
) )
def dump(self, filepath=CONFIGPATH):
### Functions ### pass
def load():
config = configparser.ConfigParser()
config.read(CONFIGPATH)
return Config.from_configparser(config)

View File

@@ -3,9 +3,9 @@ from collections import namedtuple
### Data Structures ### ### Data Structures ###
# A (string, int) tuple # A (string, int) tuple
Level = namedtuple('Level', 'name points') Level = namedtuple('Level', 'name points flair_template_id')
# A ([Level], Level, Level) tuple; # A ([Level], Level, Level) tuple
# previous can be empty, and exactly one of current and next can be None # previous can be empty, and exactly one of current and next can be None
LevelInfo = namedtuple('LevelInfo', 'previous current next') LevelInfo = namedtuple('LevelInfo', 'previous current next')
@@ -25,8 +25,7 @@ def user_level_info(points, levels):
past_levels, cur_level, next_level = [], None, None past_levels, cur_level, next_level = [], None, None
for level in levels: for level in levels:
lvlname, lvlpoints = level if points < level.points:
if points < lvlpoints:
next_level = level next_level = level
break break
if cur_level: if cur_level:

View File

@@ -2,19 +2,22 @@ from . import level
### Globals ### ### Globals ###
EMPTY_SYMBOL = '\u25AF' # A same-sized empty box character # Progress bar symbols
FILLED_SYMBOL = '\u25AE' # A small filled box character
EXCESS_SYMBOL = '\u2B51' # A star character
DIV_SYMBOL = '|' DIV_SYMBOL = '|'
FILLED_SYMBOL = '\u25AE' # A small filled box character
EMPTY_SYMBOL = '\u25AF' # A same-sized empty box character
# Number of "excess" points should be greater than max level points # Number of "excess" points should be greater than max level points
EXCESS_POINTS = 100 # TODO move this to level? EXCESS_POINTS = 100 # TODO move this to level?
EXCESS_SYMBOL = '\u2605' # A star character
EXCESS_SYMBOL_TITLE = 'a star' # Used in comment body
### Main Functions ### ### Main Functions ###
def make(redditor, points, level_info): def make(redditor, points, level_info):
paras = [header()] paras = [header()]
if points == 1: if points == 1:
paras.append(first_greeting(redditor)) paras.append(first_greeting(redditor))
if level_info.current and points == level_info.current.points: if level_info.current and points == level_info.current.points:
@@ -22,12 +25,22 @@ def make(redditor, points, level_info):
level_info.current.name, level_info.current.name,
tag_user=False)) tag_user=False))
elif points > 1: elif points > 1:
user_already_tagged = False
if level_info.current and points == level_info.current.points: if level_info.current and points == level_info.current.points:
paras.append(level_up(redditor, level_info.current.name)) paras.append(level_up(redditor,
elif not level_info.next and points > 0 and points % EXCESS_POINTS == 0: level_info.current.name,
first_star = (points == EXCESS_POINTS) tag_user=(not user_already_tagged)))
paras.append(new_star(redditor, first_star)) user_already_tagged = True
else:
if points % EXCESS_POINTS == 0:
first_excess = (points == EXCESS_POINTS)
paras.append(new_excess_symbol(redditor,
first_excess=first_excess,
tag_user=(not user_already_tagged)))
user_already_tagged = True
if not user_already_tagged:
paras.append(normal_greeting(redditor)) paras.append(normal_greeting(redditor))
paras.append(points_status(redditor, points, level_info)) paras.append(points_status(redditor, points, level_info))
@@ -43,9 +56,8 @@ def header():
def first_greeting(redditor): def first_greeting(redditor):
msg = (f'Congrats, u/{redditor.name}, you have received a point! Points ' return (f'Congrats, u/{redditor.name}, you have received a point! Points '
'help you "level up" to the next user flair!') 'help you "level up" to the next user flair!')
return msg
def normal_greeting(redditor): def normal_greeting(redditor):
@@ -58,11 +70,12 @@ def level_up(redditor, level_name, tag_user=True):
'updated accordingly.') 'updated accordingly.')
def new_star(redditor, first_star): def new_excess_symbol(redditor, first_excess=True, tag_user=True):
num_stars_msg = '' if first_star else 'another ' # Surrounding spaces for simplicity
return (f'Congrats u/{redditor.name} on getting ' tag = f' u/{redditor.name} ' if tag_user else ' '
'{num_stars_msg}{EXCESS_POINTS} points! They are shown as a star ' num_stars_prefix = ' another ' if not first_excess else ' '
'in your progress bar.') return (f'Congrats{tag}on getting{num_stars_prefix}{EXCESS_POINTS} points! '
f'They are shown as {EXCESS_SYMBOL_TITLE} in your progress bar.')
def points_status(redditor, points, level_info): def points_status(redditor, points, level_info):