Changed license and refactored

This commit is contained in:
Collin R
2020-01-15 17:42:49 -08:00
parent 3f2d754756
commit 474ea09617
9 changed files with 188 additions and 794 deletions

View File

@@ -1,18 +1 @@
'''
A bot for Reddit to award points to helpful subreddit members.
Copyright (C) 2020 Collin U. Rapp
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
from .bot import run

View File

@@ -1,20 +1,3 @@
'''
A bot for Reddit to award points to helpful subreddit members.
Copyright (C) 2020 Collin U. Rapp
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
from . import bot
if __name__ == '__main__':

View File

@@ -1,20 +1,4 @@
'''
A bot for Reddit to award points to helpful subreddit members.
Copyright (C) 2020 Collin U. Rapp
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import configparser
import re
import praw
@@ -23,38 +7,57 @@ from . import comment, database
### Globals ###
CONFIGPATH = 'pointsbot.ini'
# SUBREDDIT_NAME = 'MinecraftHelp'
SUBREDDIT_NAME = 'GlipGlorp7BotTests'
PRAW_SITE_NAME = 'testbot'
# PRAW_SITE_NAME = 'bot'
# TODO Make LEVELS a dict instead
# TODO Could also make a Level class or namedtuple that contains more info, e.g.
# flair css or template id
"""
LEVELS = [
('Helper', 5),
('Trusted Helper', 15),
('Super Helper', 40),
]
"""
### Main Function ###
def run():
# Connect to Reddit
reddit = praw.Reddit(site_name=PRAW_SITE_NAME)
subreddit = reddit.subreddit(SUBREDDIT_NAME)
config = configparser.ConfigParser()
config.read(CONFIGPATH)
# xdebugx
print(f'Connected to reddit as {reddit.user.me()}')
# Get the user flair levels in ascending order by point value
# TODO Make levels a dict instead
print(config.options('Levels'))
levels = []
for opt in config.options('Levels'):
levels.append((opt, config.getint('Levels', opt)))
levels.sort(key=lambda pair: pair[1])
print(levels)
# levels = [(key, int(val)) for key, val in config.items('Levels')]
# levels = sorted(levels, key=lambda keyval: keyval[1])
#levels = sorted(config.items('Levels'), key=lambda pair: pair[1])
# Connect to Reddit
reddit = praw.Reddit(site_name=config['Core']['praw_site_name'])
subreddit = reddit.subreddit(config['Core']['subreddit_name'])
print(f'Connected to Reddit as {reddit.user.me()}')
print(f'Read-only? {reddit.read_only}')
print(f'Watching subreddit {subreddit.title}')
print(f'Mod? {bool(subreddit.moderator(redditor=reddit.user.me()))}')
print(f'Is mod? {bool(subreddit.moderator(redditor=reddit.user.me()))}')
database.init()
# Initialize database
#if not database.exists():
#database.create()
# TODO pass database path instead of setting global variable
# database.DB_PATH = config['Core']['database_name']
# database.init()
db = database.Database(config['Core']['database_name'])
# The pattern to look for in comments when determining whether to award a point
# solved_pat = re.compile('!solved', re.IGNORECASE)
@@ -62,7 +65,6 @@ def run():
# Monitor new comments for confirmed solutions
for comm in subreddit.stream.comments(skip_existing=True):
# xdebugx
print('Found comment')
print(f'Comment text: "{comm.body}"')
@@ -75,11 +77,6 @@ def run():
# Search the flattened comments tree
is_first_solution = True
for subcomm in submission.comments.list():
# if (subcomm.is_submitter
# and subcomm.id != comm.id
# and not subcomm.is_root
# and solved_pat.search(subcomm.body)
# and subcomm.created_utc < comm.created_utc):
if (subcomm.id != comm.id
and marks_as_solved(subcomm, solved_pat)
and subcomm.created_utc < comm.created_utc):
@@ -90,28 +87,26 @@ def run():
if not is_first_solution:
# Skip this "!solved" comment and wait for the next
# xdebugx
print('This is not the first solution')
continue
# xdebugx
print('This is the first solution')
print_solution_info(comm)
solution = comm.parent()
solver = solution.author
print(f'Adding point for {solver.name}')
database.add_point(solver)
points = database.get_redditor_points(solver)
db.add_point(solver)
points = db.get_points(solver)
print(f'Points for {solver.name}: {points}')
# Reply to the comment containing the solution
reply_body = comment.make(solver, points, LEVELS)
reply_body = comment.make(solver, points, levels)
print(f'Replying with: "{reply_body}"')
solution.reply(reply_body)
# Check if (non-mod) user flair should be updated to new level
for levelname, levelpoints in LEVELS:
for levelname, levelpoints in levels:
# If the redditor's points total is equal to one of the levels,
# that means they just reached that level
if points == levelpoints:
@@ -122,13 +117,11 @@ def run():
print(f'Setting flair text to {levelname}')
subreddit.flair.set(solver, text=levelname)
else:
# xdebugx
print('Don\'t update flair b/c user is mod')
# Don't need to check the rest of the levels
break
else:
# xdebugx
print('Not a "!solved" comment')
@@ -139,7 +132,7 @@ def marks_as_solved(comment, solved_pattern):
'''Return True if not top-level comment, from OP, contains "!Solved"; False
otherwise.
'''
#comment.refresh()
#comment.refresh() # probably not needed, but the docs are a tad unclear
return (not comment.is_root
and comment.is_submitter
and solved_pattern.search(comment.body))

View File

@@ -1,21 +1,3 @@
'''
A bot for Reddit to award points to helpful subreddit members.
Copyright (C) 2020 Collin U. Rapp
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
### Globals ###
FILLED_SYMBOL = '\u25AE' # A small filled box character

View File

@@ -1,20 +1,3 @@
'''
A bot for Reddit to award points to helpful subreddit members.
Copyright (C) 2020 Collin U. Rapp
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import functools
import os.path
import sqlite3 as sqlite
@@ -22,9 +5,10 @@ import sqlite3 as sqlite
### Globals ###
# TODO put name/path in a config file?
DB_NAME = 'pointsbot.db'
DB_PATH = DB_NAME
# DB_NAME = 'pointsbot.db'
# DB_PATH = DB_NAME
"""
DB_SCHEMA = '''
CREATE TABLE IF NOT EXISTS redditor_points (
id TEXT UNIQUE NOT NULL,
@@ -32,10 +16,16 @@ DB_SCHEMA = '''
points INTEGER DEFAULT 0
)
'''
"""
### Decorators ###
# TODO read config here and create a closure instead of using DB_PATH?
# or could do it in the init function somehow?
# It seems like the only alternatives would be an OO aproach or global variable
"""
def transaction(func):
@functools.wraps(func)
def newfunc(*args, **kwargs):
@@ -45,15 +35,109 @@ def transaction(func):
ret = func(cursor, *args, **kwargs)
cursor.close()
if conn.in_transaction:
conn.commit()
cursor.close()
conn.close()
return ret
return newfunc
"""
# TODO create a method decorator for Database methods
def transaction(func):
@functools.wraps(func)
def newfunc(self, *args, **kwargs):
created_conn = False
if not self.conn:
self.conn = sqlite.connect(self.path)
self.conn.row_factory = sqlite.Row
self.cursor = self.conn.cursor()
created_conn = True
# ret = func(cursor, *args, **kwargs)
ret = func(self, *args, **kwargs)
if self.conn.in_transaction:
self.conn.commit()
if created_conn:
self.cursor.close()
self.conn.close()
self.cursor = self.conn = None
return ret
return newfunc
### Classes ###
class Database:
SCHEMA = '''
CREATE TABLE IF NOT EXISTS redditor_points (
id TEXT UNIQUE NOT NULL,
name TEXT UNIQUE NOT NULL,
points INTEGER DEFAULT 0
)
'''
def __init__(self, dbpath):
self.path = dbpath
self.conn = None
self.cursor = None
if not os.path.exists(self.path):
self._create()
@transaction
def _create(self):
self.cursor.execute(self.SCHEMA)
@transaction
def add_redditor(self, redditor):
insert_stmt = '''
INSERT OR IGNORE INTO redditor_points (id, name)
VALUES (:id, :name)
'''
self.cursor.execute(insert_stmt, {'id': redditor.id, 'name': redditor.name})
return self.cursor.rowcount
@transaction
def add_point(self, redditor):
points = self.get_points(redditor, add_if_none=True)
params = {'id': redditor.id, 'name': redditor.name, 'points': points + 1}
update_stmt = '''
UPDATE redditor_points
SET points = :points
WHERE id = :id AND name = :name
'''
self.cursor.execute(update_stmt, params)
return self.cursor.rowcount
@transaction
def get_points(self, redditor, add_if_none=False):
params = {'id': redditor.id, 'name': redditor.name}
select_stmt = '''
SELECT points
FROM redditor_points
WHERE id = :id AND name = :name
'''
self.cursor.execute(select_stmt, params)
row = self.cursor.fetchone() # TODO check if more than one row
if row:
points = row['points']
elif add_if_none:
points = 0
self.add_redditor(redditor)
return points
"""
### Private Functions ###
# These functions are intended for internal use, since they need to be explicity
@@ -119,3 +203,5 @@ add_redditor = transaction(_add_redditor)
add_point = transaction(_add_point)
get_redditor_points = transaction(_get_redditor_points)
"""