Changed config file format to TOML
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
praw.ini
|
||||
pointsbot.ini
|
||||
pointsbot.toml
|
||||
|
||||
*.db
|
||||
*.pyc
|
||||
|
||||
72
pointsbot.sample.toml
Normal file
72
pointsbot.sample.toml
Normal 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 = ""
|
||||
@@ -4,16 +4,25 @@ import praw
|
||||
|
||||
from . import config, database, level, reply
|
||||
|
||||
### Globals ###
|
||||
|
||||
USER_AGENT = 'PointsBot (by u/GlipGlorp7)'
|
||||
|
||||
TEST_COMMENTS = False
|
||||
|
||||
### Main Function ###
|
||||
|
||||
|
||||
def run():
|
||||
cfg = config.load()
|
||||
cfg = config.Config.load()
|
||||
levels = cfg.levels
|
||||
|
||||
# Connect to Reddit
|
||||
reddit = praw.Reddit(site_name=cfg.praw_site_name)
|
||||
subreddit = reddit.subreddit(cfg.subreddit_name)
|
||||
reddit = praw.Reddit(client_id=cfg.client_id,
|
||||
client_secret=cfg.client_secret,
|
||||
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(1, f'Read-only? {reddit.read_only}')
|
||||
@@ -21,6 +30,10 @@ def run():
|
||||
is_mod = bool(subreddit.moderator(redditor=reddit.user.me()))
|
||||
print_level(1, f'Is mod? {is_mod}')
|
||||
|
||||
if TEST_COMMENTS:
|
||||
make_comments(subreddit, levels)
|
||||
return
|
||||
|
||||
db = database.Database(cfg.database_path)
|
||||
|
||||
# The pattern that determines whether a post is marked as solved
|
||||
@@ -28,8 +41,8 @@ def run():
|
||||
solved_pat = re.compile('![Ss]olved')
|
||||
|
||||
# Monitor new comments for confirmed solutions
|
||||
# Passing pause_after=0 will bypass the internal exponential delay, but have
|
||||
# to check if any comments are returned with each query
|
||||
# Passing pause_after=0 will bypass the internal exponential delay; instead,
|
||||
# have to check if any comments are returned with each query
|
||||
for comm in subreddit.stream.comments(skip_existing=True, pause_after=0):
|
||||
if comm is None:
|
||||
continue
|
||||
@@ -54,24 +67,22 @@ def run():
|
||||
|
||||
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_body = reply.make(solver, points, level_info)
|
||||
# reply_body = reply.make(solver, points, levels)
|
||||
print_level(1, f'Replying with: "{reply_body}"')
|
||||
comm.reply(reply_body)
|
||||
|
||||
# Check if (non-mod) user flair should be updated to new level
|
||||
if level_info.current and level_info.current.points == points:
|
||||
print_level(1, f'User reached level: {level_info.current.name}')
|
||||
lvl = level_info.current
|
||||
if lvl and lvl.points == points:
|
||||
print_level(1, f'User reached level: {lvl.name}')
|
||||
if not subreddit.moderator(redditor=solver):
|
||||
print_level(2, 'Setting flair')
|
||||
print_level(3, f'Flair text: {level_info.current.name}')
|
||||
# TODO
|
||||
# print_level(3, f'Flair template ID: {}')
|
||||
subreddit.flair.set(solver, text=levelname)
|
||||
print_level(3, f'Flair text: {lvl.name}')
|
||||
print_level(3, f'Flair template ID: {lvl.flair_template_id}')
|
||||
subreddit.flair.set(solver,
|
||||
text=lvl.name,
|
||||
flair_template_id=lvl.flair_template_id)
|
||||
else:
|
||||
print_level(2, 'Solver is mod; don\'t alter flair')
|
||||
else:
|
||||
@@ -108,16 +119,6 @@ def is_first_solution(solved_comment, solved_pattern):
|
||||
return True
|
||||
|
||||
|
||||
def find_solver(comment):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
def make_flair(points, levels):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
### Debugging & Logging ###
|
||||
|
||||
|
||||
@@ -135,20 +136,7 @@ def print_solution_info(comm):
|
||||
print_level(3, f'Body: {comm.body}')
|
||||
|
||||
|
||||
def make_comments():
|
||||
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}')
|
||||
|
||||
def make_comments(subreddit, levels):
|
||||
testpoints = [1, 3, 5, 10, 15, 30, 45, 75] + list(range(100, 551, 50))
|
||||
|
||||
for sub in subreddit.new():
|
||||
|
||||
@@ -1,53 +1,59 @@
|
||||
import configparser
|
||||
import os.path
|
||||
# from os.path import abspath, dirname, join
|
||||
|
||||
import toml
|
||||
|
||||
from .level import Level
|
||||
|
||||
### Globals ###
|
||||
|
||||
ROOTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
CONFIGPATH = os.path.join(ROOTPATH, 'pointsbot.ini')
|
||||
|
||||
# TODO add default config values to pass to configparser
|
||||
CONFIGPATH = os.path.join(ROOTPATH, 'pointsbot.toml')
|
||||
|
||||
### Classes ###
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
def __init__(self, praw_site_name='', subreddit_name='', database_path='',
|
||||
levels=None):
|
||||
self.praw_site_name = praw_site_name
|
||||
self.subreddit_name = subreddit_name
|
||||
# Default config vals
|
||||
DEFAULT_DBPATH = 'pointsbot.db'
|
||||
|
||||
def __init__(self, subreddit, client_id, client_secret, username, password,
|
||||
levels, database_path=DEFAULT_DBPATH):
|
||||
self.subreddit = subreddit
|
||||
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
|
||||
def from_configparser(cls, config):
|
||||
database_path = os.path.join(ROOTPATH, config['Core']['database_name'])
|
||||
def load(cls, filepath=CONFIGPATH):
|
||||
obj = toml.load(filepath)
|
||||
|
||||
# Get the user flair levels in ascending order by point value
|
||||
levels = []
|
||||
for opt in config.options('Levels'):
|
||||
name, points = opt.title(), config.getint('Levels', opt)
|
||||
levels.append(Level(name, points))
|
||||
levels.sort(key=lambda lvl: lvl.points)
|
||||
for lvl in obj['levels']:
|
||||
flair_template_id = lvl['flair_template_id']
|
||||
if flair_template_id == "":
|
||||
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(
|
||||
praw_site_name=config['Core']['praw_site_name'],
|
||||
subreddit_name=config['Core']['subreddit_name'],
|
||||
obj['core']['subreddit'],
|
||||
obj['credentials']['client_id'],
|
||||
obj['credentials']['client_secret'],
|
||||
obj['credentials']['username'],
|
||||
obj['credentials']['password'],
|
||||
levels,
|
||||
database_path=database_path,
|
||||
levels=levels,
|
||||
)
|
||||
|
||||
|
||||
### Functions ###
|
||||
|
||||
|
||||
def load():
|
||||
config = configparser.ConfigParser()
|
||||
config.read(CONFIGPATH)
|
||||
return Config.from_configparser(config)
|
||||
def dump(self, filepath=CONFIGPATH):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ from collections import namedtuple
|
||||
### Data Structures ###
|
||||
|
||||
# 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
|
||||
LevelInfo = namedtuple('LevelInfo', 'previous current next')
|
||||
|
||||
@@ -25,8 +25,7 @@ def user_level_info(points, levels):
|
||||
past_levels, cur_level, next_level = [], None, None
|
||||
|
||||
for level in levels:
|
||||
lvlname, lvlpoints = level
|
||||
if points < lvlpoints:
|
||||
if points < level.points:
|
||||
next_level = level
|
||||
break
|
||||
if cur_level:
|
||||
|
||||
@@ -2,19 +2,22 @@ from . import level
|
||||
|
||||
### Globals ###
|
||||
|
||||
EMPTY_SYMBOL = '\u25AF' # A same-sized empty box character
|
||||
FILLED_SYMBOL = '\u25AE' # A small filled box character
|
||||
EXCESS_SYMBOL = '\u2B51' # A star character
|
||||
# Progress bar symbols
|
||||
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
|
||||
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 ###
|
||||
|
||||
|
||||
def make(redditor, points, level_info):
|
||||
paras = [header()]
|
||||
|
||||
if points == 1:
|
||||
paras.append(first_greeting(redditor))
|
||||
if level_info.current and points == level_info.current.points:
|
||||
@@ -22,12 +25,22 @@ def make(redditor, points, level_info):
|
||||
level_info.current.name,
|
||||
tag_user=False))
|
||||
elif points > 1:
|
||||
user_already_tagged = False
|
||||
|
||||
if level_info.current and points == level_info.current.points:
|
||||
paras.append(level_up(redditor, level_info.current.name))
|
||||
elif not level_info.next and points > 0 and points % EXCESS_POINTS == 0:
|
||||
first_star = (points == EXCESS_POINTS)
|
||||
paras.append(new_star(redditor, first_star))
|
||||
else:
|
||||
paras.append(level_up(redditor,
|
||||
level_info.current.name,
|
||||
tag_user=(not user_already_tagged)))
|
||||
user_already_tagged = True
|
||||
|
||||
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(points_status(redditor, points, level_info))
|
||||
@@ -43,9 +56,8 @@ def header():
|
||||
|
||||
|
||||
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!')
|
||||
return msg
|
||||
|
||||
|
||||
def normal_greeting(redditor):
|
||||
@@ -58,11 +70,12 @@ def level_up(redditor, level_name, tag_user=True):
|
||||
'updated accordingly.')
|
||||
|
||||
|
||||
def new_star(redditor, first_star):
|
||||
num_stars_msg = '' if first_star else 'another '
|
||||
return (f'Congrats u/{redditor.name} on getting '
|
||||
'{num_stars_msg}{EXCESS_POINTS} points! They are shown as a star '
|
||||
'in your progress bar.')
|
||||
def new_excess_symbol(redditor, first_excess=True, tag_user=True):
|
||||
# Surrounding spaces for simplicity
|
||||
tag = f' u/{redditor.name} ' if tag_user else ' '
|
||||
num_stars_prefix = ' another ' if not first_excess else ' '
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user