diff options
| -rwxr-xr-x[-rw-r--r--] | .dockerignore | 113 | ||||
| -rwxr-xr-x | .gitignore | 1 | ||||
| -rwxr-xr-x | Dockerfile | 5 | ||||
| -rwxr-xr-x | api_plotter.py | 40 | ||||
| -rwxr-xr-x | config.json.example | 38 | ||||
| -rwxr-xr-x | cron/Dockerfile | 14 | ||||
| -rwxr-xr-x | cron/daily.py | 94 | ||||
| -rw-r--r-- | cron/entrypoint.sh | 4 | ||||
| -rw-r--r-- | cron/hourly.py | 50 | ||||
| -rwxr-xr-x | cron/requirements.txt | 3 | ||||
| -rwxr-xr-x | database.py | 7 | ||||
| -rwxr-xr-x | docker-compose.yml | 22 | ||||
| -rwxr-xr-x | exampleconfig.json | 37 | ||||
| -rwxr-xr-x | onceaday/Dockerfile | 10 | ||||
| -rwxr-xr-x | onceaday/crontab | 1 | ||||
| -rwxr-xr-x | onceaday/graph.py | 31 | ||||
| -rwxr-xr-x | onceaday/onceaday.py | 94 | ||||
| -rwxr-xr-x | onceaday/requirements.txt | 1 | ||||
| -rwxr-xr-x | requirements.txt | 1 | ||||
| -rwxr-xr-x | subreddit.py | 174 | ||||
| -rw-r--r-- | youtubeInfoComment.j2 | 23 | ||||
| -rwxr-xr-x | ytapi.py | 7 |
22 files changed, 441 insertions, 329 deletions
diff --git a/.dockerignore b/.dockerignore index 26ceb08..2107721 100644..100755 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,115 @@ +logs/ +login.py +graph.png +pid.txt +ch.py +chblacklist.txt +api.log config.json +*.log +*.db +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.vs/ +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ @@ -8,6 +8,7 @@ api.log config.json *.log *.db +*.sql* # Byte-compiled / optimized / DLL files __pycache__/ @@ -1,8 +1,9 @@ FROM ubuntu:22.04 -MAINTAINER Eden Attenborough "eddie.atten.ea29@gmail.com" ARG DEBIAN_FRONTEND=noninteractive +ENV TZ=Europe/London +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN apt-get update -y -RUN apt-get install -y python3-pip build-essential libjpeg-dev zlib1g-dev +RUN apt-get install -y python3-pip build-essential tzdata COPY . /app WORKDIR /app RUN pip3 install -r requirements.txt diff --git a/api_plotter.py b/api_plotter.py deleted file mode 100755 index ba197e2..0000000 --- a/api_plotter.py +++ /dev/null @@ -1,40 +0,0 @@ -import matplotlib.pyplot as plt -import datetime -import math -import os -import re - -def round_to_min(dt: datetime.datetime): - return datetime.datetime( - year = dt.year, - month = dt.month, - day = dt.day, - hour = dt.hour, - minute = math.floor(dt.minute) - ) - -with open(os.path.join("logs", "api.log"), "r") as f: - s = f.read().split("\n") - -timestamps = set() - -for l in s: - x = re.search(r"^.*\tResponse: 200", l) - if x is not None: - timestamps.add(datetime.datetime.strptime(x.group()[1:24], "%Y-%m-%d %H:%M:%S,%f")) - -d = {} -for timestamp in timestamps: - nearest = round_to_min(timestamp) - - try: - d[nearest] += 1 - except KeyError: - d[nearest] = 1 - -d_sorted = {k: v for k, v in sorted(d.items(), key=lambda x: x[0])} - -fig, ax = plt.subplots() -ax.plot(list(d_sorted.keys()), list(d_sorted.values())) - -plt.show()
\ No newline at end of file diff --git a/config.json.example b/config.json.example new file mode 100755 index 0000000..6349b60 --- /dev/null +++ b/config.json.example @@ -0,0 +1,38 @@ +{ + "discord_webhook": "https://discord.com/api/webhooks/*********************/******************************************************", + "redditapi": + { + "client_id": "***************", + "client_secret": "************************", + "user_agent": "SmallYTChannelBot", + "username": "SmallYTChannelBot", + "password": "*******************" + }, + "imgurapi": + { + "client_id": "***************", + "client_secret": "********************************" + }, + "youtubeapi": + { + "developer_key": "******************************" + }, + "subreddit": "SmallYTChannel", + "comment_tail": "\n\n\n ^/u/SmallYTChannelBot ^*made* ^*by* ^/u/jwnskanzkwk. ^*For* ^*more* ^*information,* ^*read* ^*the* ^[FAQ.](https://www.reddit.com/user/SmallYTChannelBot/comments/a4u7qj/smallytchannelbot_faq/)", + "free_flairs": + [ + "Discussion", + "Meta", + "Collab" + ], + "mysql": + { + "host": "mysql", + "port": 3306, + "user": "root", + "passwd": "******************", + "database": "SmallYTChannel" + }, + "min_comment_len": 120, + "lambda_cost": 1 +} diff --git a/cron/Dockerfile b/cron/Dockerfile new file mode 100755 index 0000000..0239a65 --- /dev/null +++ b/cron/Dockerfile @@ -0,0 +1,14 @@ +FROM reg.reaweb.uk/smallytchannelbot +MAINTAINER Eden Attenborough "eddie.atten.ea29@gmail.com" +ARG DEBIAN_FRONTEND=noninteractive +ENV TZ=Europe/London +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +RUN apt-get install -y tzdata cron mariadb-client +COPY . /app +WORKDIR /app +RUN pip3 install -r cron/requirements.txt + +RUN echo "@daily root python3 /app/cron/daily.py > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/crontab +RUN echo "@hourly root python3 /app/cron/hourly.py > /proc/1/fd/1 2>/proc/1/fd/2" >> /etc/crontab +ENTRYPOINT ["bash"] +CMD ["/app/cron/entrypoint.sh"] diff --git a/cron/daily.py b/cron/daily.py new file mode 100755 index 0000000..8e26093 --- /dev/null +++ b/cron/daily.py @@ -0,0 +1,94 @@ +import os +import sys + +sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..")) + +from operator import itemgetter +import subreddit +import database +import datetime +# import graph + +def main(): + subreddit.display("Starting every day program...") + subreddit.display("Updating database statistics...") + with database.Database() as db: + db.update_stats() + subreddit.display("Posting and updating wiki...") + # update_tables(db.get_scores(), db.get_stats()) + + subreddit.display("Formatting leaderboard...") + leaderboard = format_monthly_leaderboard() + subreddit.display("Made the following leaderboard:\n======================\n%s\n======================\n" % leaderboard) + subreddit.display("Updating sidebar...") + #it'd be cool to find a way to access this directly without iteration + for widget in subreddit.SUBREDDIT.widgets.sidebar: + if widget.shortName == "Monthly Lambda Leaderboard": + widget.mod.update(text = leaderboard) + subreddit.display("Updated in new reddit...") + + sidebar = subreddit.SUBREDDIT.mod.settings()["description"] + oldtable = sidebar.split("------")[-1] + subreddit.SUBREDDIT.wiki['config/sidebar'].edit(content = sidebar.replace(oldtable, "\n\n## Monthly Lambda Leaderboard\n\n" + leaderboard)) + subreddit.display("Updated in old reddit...") + subreddit.display("Completed.") + + subreddit.logging.info("Called OAD prog @ %s" % subreddit.get_time()) + +# def update_tables(scores, data): +# # really ought to switch to jinja for this... +# content = "" +# date = str(datetime.date.today()) +# mods = get_mods() +# # imagepath = graph.make_graph(data) +# # imageurl = upload_image(imagepath, date) +# bylambda = [i for i in sorted(scores, key = itemgetter(1), reverse = True) if i[0] not in mods][:10] +# byhelps = sorted(scores, key = itemgetter(2), reverse = True)[:10] + +# content += "\n\n##/r/SmallYTChannel lambda tables: %s" % date + +# content += "\n\n###By lambda:" +# content += "\n\nUsername|Lambda|Help given\n:--|:--|:--" +# for line in bylambda: +# content += "\n/u/%s|%i|%i" % (line[0], line[1], line[2]) + +# content += "\n\n###By Help given:" +# content += "\n\nUsername|Lambda|Help given\n:--|:--|:--" +# for line in byhelps: +# λ = str(line[1]) +# if line[0] in mods: +# λ = "∞" +# content += "\n/u/%s|%s|%i" % (line[0], λ, line[2]) + +# # content += "\n\n##Statistics from %s:\n\nIf you're looking at this through the wiki, not through the bot's profile, then" % (date) +# # content += "the most up-to-date graph will be shown below. To see the graph at this date, follow [this link.](%s)" % (imageurl) +# content += "\n\n\n\nTotal λ in circulation|Useful advice given|Unique users\n:--|:--|:--\n%i|%i|%i" % (data[-1][1], data[-1][2], data[-1][3]) + +# # subreddit.REDDIT.subreddit("u_SmallYTChannelBot").submit("/r/SmallYTChannel Statistics: %s" % date, url = imageurl).reply(content) + + +def get_mods(): + return [str(i) for i in subreddit.SUBREDDIT.moderator()] + ["AutoModerator"] + +def format_monthly_leaderboard(): + with database.Database() as db: + leaderboard = db.get_lambda_leaderboard() + out = "**Username**|**Medal**|**Times Helped**|**Lambda**\n:-|:-|:-|:-\n" + for username, times_helped, λ in leaderboard: + out += "/u/%s|%1s|%s|%sλ\n" % (username, subreddit.get_medal(λ)[:-1], times_helped, λ) + return out + "\nLast updated: %s" % subreddit.get_time() + +# def upload_image(path, date): +# config = { +# 'album': None, +# 'name': 'SmallYTChannelBot Statistics graph: %s' % date, +# 'title': 'SmallYTChannelBot Statistics graph: %s' % date, +# 'description': 'SmallYTChannelBot Statistics graph: %s' % date +# } + +# image = subreddit.IMGUR.upload_from_path(path, config = config) + +# return "https://i.imgur.com/%s.png" % image["id"] + +if __name__ == "__main__": + main() diff --git a/cron/entrypoint.sh b/cron/entrypoint.sh new file mode 100644 index 0000000..610bf84 --- /dev/null +++ b/cron/entrypoint.sh @@ -0,0 +1,4 @@ +# https://stackoverflow.com/questions/27771781/how-can-i-access-docker-set-environment-variables-from-a-cron-job/35088810#35088810 +printenv | grep -v "no_proxy" >> /etc/environment + +cron -f
\ No newline at end of file diff --git a/cron/hourly.py b/cron/hourly.py new file mode 100644 index 0000000..89a6027 --- /dev/null +++ b/cron/hourly.py @@ -0,0 +1,50 @@ +import os +import sys + +sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..")) + +from matplotlib.ticker import EngFormatter +from discord_webhook import DiscordWebhook +from operator import itemgetter +import subprocess +import subreddit +import database +import datetime +import time + +def dump(): + subprocess.run(["rm", "-fv", "/tmp/*.sql*"]) + proc1 = subprocess.Popen( + [ + "mysqldump", subreddit.CONFIG["mysql"]["database"], + "--ignore-table", "SmallYTChannel.log", "--verbose", + "-u", subreddit.CONFIG["mysql"]["user"], + "-h", subreddit.CONFIG["mysql"]["host"], + "-p%s" % subreddit.CONFIG["mysql"]["passwd"] + ], + stdout = subprocess.PIPE + ) + proc2 = subprocess.Popen("gzip > /tmp/sytc_nolog.sql.gz", shell = True, stdin = proc1.stdout, stdout = subprocess.PIPE) + output = proc2.communicate() + +def push(fp = "/tmp/sytc_nolog.sql.gz"): + webhook = DiscordWebhook( + url = subreddit.CONFIG["discord_webhook"], + content = "Hourly /u/SmallYTChannelBot database dump from %s" % datetime.datetime.now().astimezone().isoformat() + ) + + with open(fp, "rb") as f: + webhook.add_file(file = f.read(), filename = os.path.split(fp)[-1]) + + response = webhook.execute() + subreddit.display(str(response)) + +if __name__ == "__main__": + fmt = EngFormatter("B") + starttime = time.time() + dump() + subreddit.display("\n\nDumped %s in %.1fs\n" % (fmt(os.path.getsize("/tmp/sytc_nolog.sql.gz")), time.time() - starttime)) + starttime = time.time() + push() + subreddit.display("Pushed to discord in %.2fs\n" % (time.time() - starttime)) + diff --git a/cron/requirements.txt b/cron/requirements.txt new file mode 100755 index 0000000..86f552b --- /dev/null +++ b/cron/requirements.txt @@ -0,0 +1,3 @@ +numpy==1.26.4 +matplotlib +discord-webhook diff --git a/database.py b/database.py index b80b548..300b224 100755 --- a/database.py +++ b/database.py @@ -28,12 +28,7 @@ class Database: commit (bool, optional): autocommit. Defaults to True. """ def get_date(stri): - # strip microseconds - stri = stri.split(",")[0] - try: - return datetime.datetime.strptime(stri, "%Y-%m-%d %H:%M:%S") - except ValueError: - return datetime.datetime.strptime(stri, "%b %d %Y %H:%M:%S") + return datetime.datetime.fromisoformat(stri) addFlag = False s = line.split("\t") diff --git a/docker-compose.yml b/docker-compose.yml index 900c7f8..62945b8 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,34 @@ -version: '3' - services: sytc: build: context: . dockerfile: Dockerfile - image: r.vm.gl/smallytchannelbot + image: reg.reaweb.uk/smallytchannelbot networks: - db-network external_links: - mariadb:mysql + volumes: + - ./config.json:/app/config.json + restart: unless-stopped + cron: build: context: . - dockerfile: ./onceaday/Dockerfile - image: r.vm.gl/smallytchannelbotoad + dockerfile: ./cron/Dockerfile + image: reg.reaweb.uk/smallytchannelbot_cron networks: - db-network external_links: - mariadb:mysql + depends_on: + - sytc + volumes: + - ./config.json:/app/config.json + restart: unless-stopped networks: db-network: - external: - name: mariadb + external: true + name: mariadb + diff --git a/exampleconfig.json b/exampleconfig.json deleted file mode 100755 index 0ad4134..0000000 --- a/exampleconfig.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "redditapi": - { - "client_id": "xxxxxxxxxxxxxxx", - "client_secret": "xxxxxxxxxxxxxxxxxxxxxxxx", - "user_agent": "SmallYTChannelBot", - "username": "SmallYTChannelBot", - "password": "xxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - "imgurapi": - { - "client_id": "xxxxxxxxxxxxxxxx", - "client_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - "youtubeapi": - { - "developer_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - }, - "subreddit": "SmallYTChannel", - "comment_tail": "\n\n\n ^/u/SmallYTChannelBot ^*made* ^*by* ^/u/jwnskanzkwk. ^*PM* ^*for* ^*bug* ^*reports.* ^*For* ^*more* ^*information,* ^*read* ^*the* ^[FAQ.](https://www.reddit.com/user/SmallYTChannelBot/comments/a4u7qj/smallytchannelbot_faq/)", - "free_flairs": - [ - "Discussion", - "Meta", - "Collab" - ], - "mysql": - { - "host": "localhost", - "port": 3306, - "user": "root", - "passwd": "xxxxxxxxxxxxxxxx", - "database": "SmallYTChannel" - }, - "min_comment_len": 120, - "lambda_cost": 2 -} diff --git a/onceaday/Dockerfile b/onceaday/Dockerfile deleted file mode 100755 index b6b9d91..0000000 --- a/onceaday/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM r.vm.gl/smallytchannelbot -MAINTAINER Eden Attenborough "eddie.atten.ea29@gmail.com" -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get install -y tzdata cron -COPY . /app -WORKDIR /app -COPY onceaday/crontab /etc/cron.d/oad-crontab -RUN chmod 0644 /etc/cron.d/oad-crontab && crontab /etc/cron.d/oad-crontab -RUN pip3 install -r onceaday/requirements.txt -ENTRYPOINT ["cron", "-f"] diff --git a/onceaday/crontab b/onceaday/crontab deleted file mode 100755 index c8ce32a..0000000 --- a/onceaday/crontab +++ /dev/null @@ -1 +0,0 @@ -@daily python3 /app/onceaday/onceaday.py /app/onceaday diff --git a/onceaday/graph.py b/onceaday/graph.py deleted file mode 100755 index 723997b..0000000 --- a/onceaday/graph.py +++ /dev/null @@ -1,31 +0,0 @@ -from mpl_toolkits.axes_grid1 import host_subplot -import mpl_toolkits.axisartist as AA -import matplotlib.pyplot as plt -import matplotlib -import datetime - -def make_graph(data): - fig = plt.figure() - - lambdaCount = [i[1] for i in data] - helpGiven = [i[2] for i in data] - uniqueUsers = [i[3] for i in data] - date = [datetime.datetime.strptime(i[4], "%Y-%m-%d") for i in data] - - fig, ax1 = plt.subplots() - ax1.plot(date, lambdaCount, label = "Total λ in circulation", color = "r") - ax1.set_ylabel("Total λ / help given") - - ax1.plot(date, helpGiven, label = "Times help given", color = "g") - - ax2 = ax1.twinx() - ax2.plot(date, uniqueUsers, label = "Unique users") - ax2.set_ylabel("No. Unique Users") - - ax1.legend() - ax2.legend(loc = 4) - fig.autofmt_xdate() - - filepath = "graph.png" - fig.savefig(filepath) - return filepath
\ No newline at end of file diff --git a/onceaday/onceaday.py b/onceaday/onceaday.py deleted file mode 100755 index 7762d05..0000000 --- a/onceaday/onceaday.py +++ /dev/null @@ -1,94 +0,0 @@ -import os -import sys - -os.chdir(sys.argv[1]) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) - -from operator import itemgetter -import subreddit -import database -import datetime -import graph - -def main(): - subreddit.display("Starting every day program...") - subreddit.display("Updating database statistics...") - with database.Database() as db: - db.update_stats() - subreddit.display("Posting and updating wiki...") - update_tables(db.get_scores(), db.get_stats()) - - subreddit.display("Formatting leaderboard...") - leaderboard = format_monthly_leaderboard() - subreddit.display("Made the following leaderboard:\n======================\n%s\n======================\n" % leaderboard) - subreddit.display("Updating sidebar...") - #it'd be cool to find a way to access this directly without iteration - for widget in subreddit.SUBREDDIT.widgets.sidebar: - if widget.shortName == "Monthly Lambda Leaderboard": - widget.mod.update(text = leaderboard) - subreddit.display("Updated in new reddit...") - - sidebar = subreddit.SUBREDDIT.mod.settings()["description"] - oldtable = sidebar.split("------")[-1] - subreddit.SUBREDDIT.wiki['config/sidebar'].edit(content = sidebar.replace(oldtable, "\n\n## Monthly Lambda Leaderboard\n\n" + leaderboard)) - subreddit.display("Updated in old reddit...") - subreddit.display("Completed.") - - subreddit.logging.info("Called OAD prog @ %s" % subreddit.get_time()) - -def update_tables(scores, data): - content = "" - date = str(datetime.date.today()) - mods = get_mods() - imagepath = graph.make_graph(data) - imageurl = upload_image(imagepath, date) - bylambda = [i for i in sorted(scores, key = itemgetter(1), reverse = True) if i[0] not in mods][:10] - byhelps = sorted(scores, key = itemgetter(2), reverse = True)[:10] - - content += "\n\n##/r/SmallYTChannel lambda tables: %s" % date - - content += "\n\n###By lambda:" - content += "\n\nUsername|Lambda|Help given\n:--|:--|:--" - for line in bylambda: - content += "\n/u/%s|%i|%i" % (line[0], line[1], line[2]) - - content += "\n\n###By Help given:" - content += "\n\nUsername|Lambda|Help given\n:--|:--|:--" - for line in byhelps: - λ = str(line[1]) - if line[0] in mods: - λ = "∞" - content += "\n/u/%s|%s|%i" % (line[0], λ, line[2]) - - content += "\n\n##Statistics from %s:\n\nIf you're looking at this through the wiki, not through the bot's profile, then" % (date) - content += "the most up-to-date graph will be shown below. To see the graph at this date, follow [this link.](%s)" % (imageurl) - content += "\n\n\n\nTotal λ in circulation|Useful advice given|Unique users\n:--|:--|:--\n%i|%i|%i" % (data[-1][1], data[-1][2], data[-1][3]) - - subreddit.REDDIT.subreddit("u_SmallYTChannelBot").submit("/r/SmallYTChannel Statistics: %s" % date, url = imageurl).reply(content) - - -def get_mods(): - return [str(i) for i in subreddit.SUBREDDIT.moderator()] + ["AutoModerator"] - -def format_monthly_leaderboard(): - with database.Database() as db: - leaderboard = db.get_lambda_leaderboard() - out = "**Username**|**Medal**|**Times Helped**|**Lambda**\n:-|:-|:-|:-\n" - for username, times_helped, λ in leaderboard: - out += "/u/%s|%1s|%s|%sλ\n" % (username, subreddit.get_medal(λ)[:-1], times_helped, λ) - return out + "\nLast updated: %s" % subreddit.get_time() - -def upload_image(path, date): - config = { - 'album': None, - 'name': 'SmallYTChannelBot Statistics graph: %s' % date, - 'title': 'SmallYTChannelBot Statistics graph: %s' % date, - 'description': 'SmallYTChannelBot Statistics graph: %s' % date - } - - image = subreddit.IMGUR.upload_from_path(path, config = config) - - return "https://i.imgur.com/%s.png" % image["id"] - -if __name__ == "__main__": - main() diff --git a/onceaday/requirements.txt b/onceaday/requirements.txt deleted file mode 100755 index e3a623c..0000000 --- a/onceaday/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -matplotlib==3.3.4 diff --git a/requirements.txt b/requirements.txt index 7f1d56a..958ab64 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ js2py praw sqlite3-to-mysql pymysql +jinja2 diff --git a/subreddit.py b/subreddit.py index 9fe4088..0360dea 100755 --- a/subreddit.py +++ b/subreddit.py @@ -1,8 +1,13 @@ -from imgurpython import ImgurClient +# from imgurpython import ImgurClient from operator import itemgetter + +import praw.models +import praw.models.reddit +import praw.models.reddit.comment import database import datetime import logging +import jinja2 import ytapi # import graph import time @@ -11,10 +16,10 @@ import json import re import os -if os.path.split(os.getcwd())[-1] == "onceaday": - configpath = "../config.json" -else: - configpath = "config.json" + +configpath = os.path.join(os.path.dirname(__file__), "config.json") +if not os.path.exists(configpath): + configpath = os.path.join(os.path.dirname(__file__), "..", "config.json") with open(configpath, "r") as f: CONFIG = json.load(f) @@ -25,10 +30,10 @@ SUBREDDIT = REDDIT.subreddit(CONFIG["subreddit"]) COMMENT_TAIL = CONFIG["comment_tail"] FREE_FLAIRS = CONFIG["free_flairs"] -IMGUR = ImgurClient(**CONFIG["imgurapi"]) +# IMGUR = ImgurClient(**CONFIG["imgurapi"]) logging.basicConfig( - format = "%(process)s\t[%(asctime)s]\t%(message)s", + format = "%(message)s", level = logging.INFO, handlers=[ logging.FileHandler("actions.log"), @@ -44,7 +49,8 @@ logging.basicConfig( # logger.addHandler(handler) def get_time(): - return time.strftime("%b %d %Y %H:%M:%S", time.gmtime()) + # return time.strftime("%b %d %Y %H:%M:%S", time.gmtime()) + return datetime.datetime.now().astimezone().isoformat() def display(message, concerning = None): logging.info(message) @@ -62,9 +68,9 @@ def get_lambda_from_flair(s): else: return "" -def update_users_flair_from_comment(comment, reddit): +def update_users_flair_from_comment(comment): #implemented only for legacy - update_users_flair(str(comment.author), reddit) + update_users_flair(str(comment.author)) def get_medal(actualscore): if actualscore >= 10 and actualscore < 25: @@ -78,14 +84,14 @@ def get_medal(actualscore): else: return "" -def update_users_flair(username, reddit): - flairtext = next(reddit.subreddit(CONFIG["subreddit"]).flair(redditor=username))["flair_text"] +def update_users_flair(username): + flairtext = next(REDDIT.subreddit(CONFIG["subreddit"]).flair(redditor=username))["flair_text"] if flairtext is None: flairtext = "" else: flairscore = get_lambda_from_flair(flairtext) flairtext = str(flairtext.replace("[%s] " % flairscore, "")) - if username in get_mods(reddit): + if username in get_mods(): newflair = "[🏆 ∞λ] %s" % (flairtext) else: with database.Database() as db: @@ -93,16 +99,16 @@ def update_users_flair(username, reddit): newflair = "[%s%iλ] %s" % (get_medal(actualscore), actualscore, flairtext) logging.info("/u/%s had their flair updated" % username) - reddit.subreddit(CONFIG["subreddit"]).flair.set(redditor = username, text = newflair) + REDDIT.subreddit(CONFIG["subreddit"]).flair.set(redditor = username, text = newflair) -def get_mods(reddit): - return [str(i) for i in reddit.subreddit(CONFIG["subreddit"]).moderator()] + ["AutoModerator"] +def get_mods(): + return [str(i) for i in REDDIT.subreddit(CONFIG["subreddit"]).moderator()] + ["AutoModerator"] -def handle_mylambda(comment, reddit): +def handle_mylambda(comment: praw.models.reddit.comment): author = str(comment.author) with database.Database() as db: λ, links = db.get_lambda(author) - if author in get_mods(reddit): + if author in get_mods(): text = "/u/%s is a moderator, and therefore has ∞λ." % author else: text = "/u/%s currently has %iλ, and has helped helping the following posts:" % (author, λ) @@ -119,10 +125,10 @@ def handle_mylambda(comment, reddit): text += "\n\n[%i more...]" % len(links) - count break - update_users_flair_from_comment(comment, reddit) + update_users_flair_from_comment(comment) return text -def handle_givelambda(comment, reddit): +def handle_givelambda(comment: praw.models.reddit.comment): submission = comment.submission parentauthour = str(comment.parent().author) op = str(comment.author) @@ -131,7 +137,7 @@ def handle_givelambda(comment, reddit): text = "You cannot give yourself λ." elif parentauthour == "SmallYTChannelBot": text = "Please only give lambda to humans." - elif str(comment.author) in get_mods(reddit): + elif str(comment.author) in get_mods(): text = "The moderator /u/%s has given /u/%s 1λ. /u/%s now has %iλ." % (str(comment.author), parentauthour, parentauthour, db.get_lambda(parentauthour)[0] + 1) db.give_lambda(parentauthour, submission.permalink, timestamp = int(submission.created_utc)) display(text, concerning=comment.permalink) @@ -158,10 +164,10 @@ def handle_givelambda(comment, reddit): db.give_lambda(parentauthour, submission.permalink, timestamp = int(submission.created_utc)) # update_users_flair_from_comment(comment) - update_users_flair_from_comment(comment.parent(), reddit) + update_users_flair_from_comment(comment.parent()) return text -def handle_setlambda(comment, reddit): +def handle_setlambda(comment: praw.models.reddit.comment): try: splitted = comment.body.split() user = splitted[1].replace("/u/", "").replace("u/", "") @@ -170,17 +176,18 @@ def handle_setlambda(comment, reddit): with database.Database() as db: text = "/u/%s had their lambda set to %iλ by a moderator." % (user, toset) db.set_lambda(user, toset) - display("A moderator set /u/%s λ to %d" % (user, toset), concerning=comment.permalink) + display("A moderator set /u/%s λ to %d" % (user, toset), concerning = comment.permalink) except Exception as e: - display("{ERROR while setting λ} %s" % e, concerning=comment.permalink) + display("{ERROR while setting λ} %s" % e, concerning = comment.permalink) text = r"An error was encountered. Please use the syntax `!setlambda <user> <set to {integer}>`" + "\n\nThe error was:\n\n" + str(e) - update_users_flair(user, reddit) + update_users_flair(user) return text -def handle_submission(submission, reddit): +def handle_submission(submission: praw.models.reddit.submission): with database.Database() as db: score = db.get_lambda(str(submission.author))[0] + if submission.link_flair_text in FREE_FLAIRS: if "youtube.com" in str(submission.url) or "youtu.be" in str(submission.url): text = "Your post has been removed because it has the wrong flair. [Discussion], [Meta] and [Collab] flairs are only for text submissions." @@ -191,9 +198,9 @@ def handle_submission(submission, reddit): else: if score < CONFIG["lambda_cost"]: text = """Thank you for submitting to /r/SmallYTChannel. Unfortunally, you submission has been removed since you do not have enough λ. You need - %iλ to post. You currently have %iλ. For more information, read the [FAQ.](https://www.reddit.com/user/SmallYTChannelBot/comments/a4u7qj/smallytchannelbot_faq/)""" % score + %iλ to post. You currently have %iλ. For more information, read the [FAQ.](https://www.reddit.com/user/SmallYTChannelBot/comments/a4u7qj/smallytchannelbot_faq/)""" % (CONFIG["lambda_cost"], score) submission.mod.remove() - display("/u/%s had their submission removed for insufficient lambda." % (CONFIG["lambda_cost"], submission.author), concerning=submission.permalink) + display("/u/%s had their submission removed for insufficient lambda." % (submission.author), concerning=submission.permalink) else: text = """Thank you for submitting to /r/SmallYTChannel. You have spent %iλ to submit here, making your current balance %iλ. /u/%s, please comment `!givelambda` to the most helpful advice you are given. @@ -201,76 +208,54 @@ def handle_submission(submission, reddit): with database.Database() as db: db.change_lambda(str(submission.author), -CONFIG["lambda_cost"]) - try: - ytid = ytapi.get_videoId_from_url(submission.url) - if "/" not in ytid: + ytid = ytapi.get_videoId_from_url(submission.url) + if "/" not in ytid: # why is this necessary? + try: ytdata = ytapi.get_video_data(ytid) - - text += """ -\n\n\n##Video data: - -**Field**|**Data** -:-|:- -Title|%s -Thumbnail|[Link](%s) -Views|%s -Length|%s -Likes|%s -Comments|%s -Description|%s - -##Channel Data: - -**Field**|**Data** -:-|:- -Name|%s -Thumbnail|[Link](%s) -Subscribers|%s -Videos|%s -Views|%s - """ % ( - ytdata["title"], - ytdata["thumbnail"], - ytdata["views"], - ytdata["length"], - ytdata["likes"], - ytdata["comments"], - ytdata["description"], - ytdata["channel"], - ytdata["channelThumb"], - ytdata["subscribers"], - ytdata["videos"], - ytdata["channelViews"] - ) - - curflair = submission.link_flair_text - if str(curflair) != "None": - submission.mod.flair(" %s | %s | :youtube: %s" % (curflair, ytdata["length"], ytdata["channel"])) - else: - submission.mod.flair("%s | :youtube: %s" % (ytdata["length"], ytdata["channel"])) - except: - pass - - update_users_flair(str(submission.author), reddit) - return text - -def handle_comment(comment, reddit): + except Exception as e: + ytdata = ytapi.ERROR_DICT + display("{ERROR WITH YOUTUBE} %s" % e, concerning = submission.permalink) + + jinja_env = jinja2.Environment(loader = jinja2.FileSystemLoader(os.path.dirname(__file__))) + template = jinja_env.get_template("youtubeInfoComment.j2") + text += template.render(ytdata) + + curflair = submission.link_flair_text + if str(curflair) != "None": + submission.mod.flair(text = " %s | %s | :youtube: %s" % (curflair, ytdata["length"], ytdata["channel"])) + else: + submission.mod.flair(text = "%s | :youtube: %s" % (ytdata["length"], ytdata["channel"])) + else: + display("{ERROR} Didn't pin comment due to a missing slash", concerning = submission.permalink) + + # Sticking to the bottom means making it the 'second' sticked post, the first is reserved for moderator stickies. + # It has the side effect of appearing in the 'Community highlights' section (which is what we want) + # That feature seems poorely documented, not sure how many items can appear in there? Hopefully they automatically + # slide off in a queue according to age + submission.mod.sticky(bottom = True) + + update_users_flair(str(submission.author)) + reply = submission.reply(text + COMMENT_TAIL) + reply.mod.distinguish(sticky = True) + reply.mod.approve() + +def handle_comment(comment): response = None if "!mylambda" in comment.body.lower() and str(comment.author) != "SmallYTChannelBot": - response = handle_mylambda(comment, reddit) + response = handle_mylambda(comment) if "!givelambda" in comment.body.lower() and str(comment.author) != "SmallYTChannelBot": - response = handle_givelambda(comment, reddit) + response = handle_givelambda(comment) - if comment.body.startswith("!setlambda") and str(comment.author) in get_mods(reddit): - response = handle_setlambda(comment, reddit) + if comment.body.startswith("!setlambda") and str(comment.author) in get_mods(): + response = handle_setlambda(comment) if response is not None: reply = comment.reply(response + COMMENT_TAIL) reply.mod.distinguish(sticky = False) -def stream(reddit): - subreddit = reddit.subreddit(CONFIG["subreddit"]) +def stream(): + subreddit = REDDIT.subreddit(CONFIG["subreddit"]) streams = [subreddit.stream.comments(pause_after=-1), subreddit.stream.submissions(pause_after=-1)] with database.Database() as db: while True: @@ -284,22 +269,18 @@ def stream(reddit): db.add_to_blacklist(item.id) if str(type(item)) == "<class 'praw.models.reddit.comment.Comment'>": - handle_comment(item, reddit) + handle_comment(item) elif str(type(item)) == "<class 'praw.models.reddit.submission.Submission'>": display("There has been a new submission: '%s', with flair '%s'" % (item.title, item.link_flair_text), concerning=item.permalink) - if str(item.author) not in get_mods(reddit): - reply = item.reply(handle_submission(item, reddit) + COMMENT_TAIL) - reply.mod.distinguish(sticky = True) - reply.mod.approve() + if str(item.author) not in get_mods(): + handle_submission(item) time.sleep(30) def main(): - reddit = praw.Reddit(**CONFIG["redditapi"]) - reddit.validate_on_submit = True try: - stream(reddit) + stream() except Exception as e: display("{ERROR} %s" % str(e)) time.sleep(60) @@ -309,3 +290,4 @@ def main(): if __name__ == "__main__": main() + diff --git a/youtubeInfoComment.j2 b/youtubeInfoComment.j2 new file mode 100644 index 0000000..34d1647 --- /dev/null +++ b/youtubeInfoComment.j2 @@ -0,0 +1,23 @@ + + +##Video data: + +**Field**|**Data** +:-|:- +Title|{{ title }} +Thumbnail|[Link]({{ thumbnail }}) +Views|{{ views }} +Length|{{ length }} +Likes|{{ likes }} +Comments|{{ comments }} +Description|{{ description }} + +##Channel Data: + +**Field**|**Data** +:-|:- +Name|{{ channel }} +Thumbnail|[Link]({{ channelThumb }}) +Subscribers|{{ subscribers }} +Videos|{{ videos }} +Views|{{ channelViews }}
\ No newline at end of file @@ -4,10 +4,9 @@ import json import js2py import os -if os.path.split(os.getcwd())[-1] == "onceaday": - configpath = "../config.json" -else: - configpath = "config.json" +configpath = os.path.join(os.path.dirname(__file__), "config.json") +if not os.path.exists(configpath): + configpath = os.path.join(os.path.dirname(__file__), "..", "config.json") with open(configpath, "r") as f: CONFIG = json.load(f) |
