aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml17
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Pipfile5
-rw-r--r--Pipfile.lock178
-rw-r--r--README.md24
-rw-r--r--azure-pipelines.yml68
-rw-r--r--bot/__init__.py4
-rw-r--r--bot/__main__.py24
-rw-r--r--bot/cogs/__init__.py0
-rw-r--r--bot/cogs/error_handler.py106
-rw-r--r--bot/cogs/evergreen/__init__.py0
-rw-r--r--bot/cogs/evergreen/uptime.py33
-rw-r--r--bot/cogs/hacktober/__init__.py0
-rw-r--r--bot/cogs/hacktober/candy_collection.py229
-rw-r--r--bot/cogs/hacktober/hacktoberstats.py326
-rw-r--r--bot/cogs/hacktober/halloween_facts.py (renamed from bot/cogs/halloween_facts.py)7
-rw-r--r--bot/cogs/hacktober/halloweenify.py (renamed from bot/cogs/halloweenify.py)2
-rw-r--r--bot/cogs/hacktober/monstersurvey.py191
-rw-r--r--bot/cogs/hacktober/movie.py (renamed from bot/cogs/movie.py)0
-rw-r--r--bot/cogs/hacktober/spookyavatar.py52
-rw-r--r--bot/cogs/hacktober/spookyreact.py69
-rw-r--r--bot/cogs/hacktober/spookysound.py (renamed from bot/cogs/spookysound.py)11
-rw-r--r--bot/cogs/hacktoberstats.py193
-rw-r--r--bot/cogs/spookyreact.py31
-rw-r--r--bot/cogs/template.py4
-rw-r--r--bot/constants.py2
-rw-r--r--bot/resources/halloween/bat-clipart.pngbin0 -> 12313 bytes
-rw-r--r--bot/resources/halloween/bloody-pentagram.pngbin0 -> 7006 bytes
-rw-r--r--bot/resources/halloween/candy_collection.json8
-rw-r--r--bot/resources/halloween/halloween_facts.json (renamed from bot/resources/halloween_facts.json)0
-rw-r--r--bot/resources/halloween/halloweenify.json (renamed from bot/resources/halloweenify.json)0
-rw-r--r--bot/resources/halloween/monstersurvey.json30
-rw-r--r--bot/utils/__init__.py0
-rw-r--r--bot/utils/spookifications.py55
-rwxr-xr-xdocker/build.sh18
-rw-r--r--docker/docker-compose.yml6
-rwxr-xr-xscripts/deploy-azure.sh22
37 files changed, 1234 insertions, 483 deletions
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ae1d7653..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-language: python
-
-python:
- - "3.7-dev"
-
-sudo: required
-
-services:
- - docker
-
-install:
- - pip install flake8 pipenv salt-pepper
- - pipenv install --deploy
-
-script:
- - pipenv run lint
- - bash docker/build.sh
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 35c951d1..5c16e605 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,4 +11,4 @@ This project is a community project for the Python Discord community over at htt
# Installation & Dependency Management
-Hacktoberbot utilizes [Pipenv](https://pipenv.readthedocs.io/en/latest/) for installation and dependency management. For users unfamiliar with the Pipenv workflow, a [Pipenv Primer](https://github.com/discord-python/hacktoberbot/wiki/Hacktoberbot-and-Pipenv) is provided in Hactoberbot's Wiki.
+Hacktoberbot utilizes [Pipenv](https://pipenv.readthedocs.io/en/latest/) for installation and dependency management. For users unfamiliar with the Pipenv workflow, a [Pipenv Primer](https://github.com/python-discord/seasonalbot/wiki/Hacktoberbot-and-Pipenv) is provided in Seasonalbot's Wiki.
diff --git a/Pipfile b/Pipfile
index a48560f6..e2bf2b3a 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,7 +4,10 @@ verify_ssl = true
name = "pypi"
[packages]
-"discord.py" = {extras = ["voice"], ref = "rewrite", git = "https://github.com/Rapptz/discord.py"}
+"discord-py" = {ref = "rewrite", git = "https://github.com/Rapptz/discord.py"}
+arrow = "*"
+pillow = "*"
+
[dev-packages]
"flake8" = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 0c5f9524..00000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,178 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "e03524b8f202c99f589b48669be2d8f0431f47c452cacc2396b5a40bcbe268b6"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.7"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "cffi": {
- "hashes": [
- "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
- "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
- "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
- "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
- "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
- "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
- "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
- "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
- "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
- "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
- "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
- "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
- "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
- "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
- "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
- "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
- "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
- "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
- "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
- "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
- "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
- "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
- "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
- "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
- "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
- "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
- "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
- "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
- "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
- "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
- "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
- "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
- ],
- "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'",
- "version": "==1.11.5"
- },
- "discord.py": {
- "git": "https://github.com/Rapptz/discord.py",
- "ref": "860d6a9ace8248dfeec18b8b159e7b757d9f56bb"
- },
- "pycparser": {
- "hashes": [
- "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
- ],
- "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'",
- "version": "==2.19"
- },
- "pynacl": {
- "hashes": [
- "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
- "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
- "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
- "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
- "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
- "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
- "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
- "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
- "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
- "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
- "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
- "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
- "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
- "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
- "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
- "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
- "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
- "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
- "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
- ],
- "index": "pypi",
- "version": "==1.3.0"
- },
- "six": {
- "hashes": [
- "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
- "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
- ],
- "version": "==1.11.0"
- }
- },
- "develop": {
- "attrs": {
- "hashes": [
- "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
- "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
- ],
- "version": "==18.2.0"
- },
- "flake8": {
- "hashes": [
- "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
- "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
- ],
- "index": "pypi",
- "version": "==3.5.0"
- },
- "flake8-bugbear": {
- "hashes": [
- "sha256:07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83",
- "sha256:0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"
- ],
- "index": "pypi",
- "version": "==18.8.0"
- },
- "flake8-import-order": {
- "hashes": [
- "sha256:9be5ca10d791d458eaa833dd6890ab2db37be80384707b0f76286ddd13c16cbf",
- "sha256:feca2fd0a17611b33b7fa84449939196c2c82764e262486d5c3e143ed77d387b"
- ],
- "index": "pypi",
- "version": "==0.18"
- },
- "flake8-string-format": {
- "hashes": [
- "sha256:68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2",
- "sha256:774d56103d9242ed968897455ef49b7d6de272000cfa83de5814273a868832f1"
- ],
- "index": "pypi",
- "version": "==0.2.3"
- },
- "flake8-tidy-imports": {
- "hashes": [
- "sha256:5fc28c82bba16abb4f1154dc59a90487f5491fbdb27e658cbee241e8fddc1b91",
- "sha256:c05c9f7dadb5748a04b6fa1c47cb6ae5a8170f03cfb1dca8b37aec58c1ee6d15"
- ],
- "index": "pypi",
- "version": "==1.1.0"
- },
- "flake8-todo": {
- "hashes": [
- "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"
- ],
- "index": "pypi",
- "version": "==0.7"
- },
- "mccabe": {
- "hashes": [
- "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
- "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
- ],
- "version": "==0.6.1"
- },
- "pycodestyle": {
- "hashes": [
- "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
- "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
- ],
- "version": "==2.3.1"
- },
- "pyflakes": {
- "hashes": [
- "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
- "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
- ],
- "version": "==1.6.0"
- }
- }
-}
diff --git a/README.md b/README.md
index 5532ed1e..b83421ff 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,16 @@
-# hacktoberbot
-A community project for [Hacktoberfest 2018](https://hacktoberfest.digitalocean.com). A Discord bot primarily designed to help teach Python learners from the PythonDiscord community how to contribute to open source.
+# SeasonalBot
+
+[![Build Status](https://dev.azure.com/python-discord/Python%20Discord/_apis/build/status/Seasonal%20Bot%20(Mainline))](https://dev.azure.com/python-discord/Python%20Discord/_build/latest?definitionId=3)
+[![Discord](https://discordapp.com/api/guilds/267624335836053506/embed.png)](https://discord.gg/2B963hn)
+
+A community project initially started for [Hacktoberfest 2018](https://hacktoberfest.digitalocean.com). A Discord bot for the Python Discord community which changes with the seasons, and provides useful event features.
You can find our community by going to https://discord.gg/python
## Motivations
We know it can be difficult to get into the whole open source thing at first. To help out, we've decided to start a little community project during hacktober that you can all choose to contribute to if you're finding the event a little overwhelming, or if you're new to this whole thing and just want someone to hold your hand at first.
-## Commands
-
-!repository - Links to this repository
-
-### Git
-!Git - Links to getting started with Git page
-!Git.commit - An example commit command
-
-### Halloween Facts
-Random halloween facts are posted regularly.
-!hallofact - Show the last posted Halloween fact
+This later evolved into a bot that will be running all through the year, providing season-appropriate functionality and issues that beginners can work on.
## Getting started
@@ -87,9 +81,9 @@ You should get a menu like below:
![](https://i.imgur.com/xA5ga4C.png)
#### 6. Pull Requests (PR or PRs)
-Goto https://github.com/discord-python/hacktoberbot/pulls and the green New Pull Request button!
+Goto https://github.com/python-discord/seasonalbot/pulls and the green New Pull Request button!
![](https://i.imgur.com/fB4a2wQ.png)
-Now you should hit `Compare across forks` then on the third dropdown select your fork (it will be `your username/hacktoberbot`) then hit Create Pull request.
+Now you should hit `Compare across forks` then on the third dropdown select your fork (it will be `your username/seasonalbot`) then hit Create Pull request.
1[](https://i.imgur.com/N2X9A9v.png)
Now to tell other people what your PR does
1. Title - be concise and informative
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 00000000..2b43cc57
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,68 @@
+# https://aka.ms/yaml
+
+variables:
+ LIBRARY_PATH: /lib:/usr/lib
+ PIPENV_HIDE_EMOJIS: 1
+ PIPENV_IGNORE_VIRTUALENVS: 1
+ PIPENV_NOSPIN: 1
+ PIPENV_VENV_IN_PROJECT: 1
+
+jobs:
+- job: test
+ displayName: 'Lint & Test'
+
+ pool:
+ vmImage: 'Ubuntu 16.04'
+
+ variables:
+ PIPENV_CACHE_DIR: ".cache/pipenv"
+ PIP_CACHE_DIR: ".cache/pip"
+
+ steps:
+ - script: sudo apt-get install build-essential curl docker libffi-dev libfreetype6-dev libxml2 libxml2-dev libxslt1-dev zlib1g zlib1g-dev
+ displayName: 'Install base dependencies'
+
+ - task: UsePythonVersion@0
+ displayName: 'Set Python version'
+ inputs:
+ versionSpec: '3.7.x'
+ addToPath: true
+
+ - script: sudo pip install pipenv
+ displayName: 'Install pipenv'
+
+ - script: pipenv install --dev --deploy --system
+ displayName: 'Install project using pipenv'
+
+ - script: python -m flake8
+ displayName: 'Run linter'
+
+- job: build
+ displayName: 'Build Containers'
+ dependsOn: 'test'
+
+ variables:
+ PIPENV_CACHE_DIR: ".cache/pipenv"
+ PIP_CACHE_DIR: ".cache/pip"
+
+ steps:
+ - task: Docker@1
+ displayName: 'Login: Docker Hub'
+
+ inputs:
+ containerregistrytype: 'Container Registry'
+ dockerRegistryEndpoint: 'DockerHub'
+ command: 'login'
+
+ - script: sudo apt-get install python3-setuptools
+ displayName: 'Install setuptools'
+
+ - script: sudo pip3 install salt-pepper
+ displayName: 'Install pepper'
+
+ - task: ShellScript@2
+ displayName: 'Build and deploy containers'
+
+ inputs:
+ scriptPath: scripts/deploy-azure.sh
+ args: '$(SALTAPI_TARGET) $(SALTAPI_USER) $(SALTAPI_PASS) $(SALTAPI_URL)'
diff --git a/bot/__init__.py b/bot/__init__.py
index c411deb6..6b3a2a6f 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -2,6 +2,10 @@ import logging.handlers
import os
from pathlib import Path
+import arrow
+
+# start datetime
+start_time = arrow.utcnow()
# set up logging
log_dir = Path("bot", "log")
diff --git a/bot/__main__.py b/bot/__main__.py
index ccd69b0b..b74e4f54 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -5,35 +5,29 @@ from traceback import format_exc
from discord.ext import commands
-HACKTOBERBOT_TOKEN = environ.get('HACKTOBERBOT_TOKEN')
+SEASONALBOT_TOKEN = environ.get('SEASONALBOT_TOKEN')
log = logging.getLogger()
-if HACKTOBERBOT_TOKEN:
- token_dl = len(HACKTOBERBOT_TOKEN) // 8
- log.info(f'Bot token loaded: {HACKTOBERBOT_TOKEN[:token_dl]}...{HACKTOBERBOT_TOKEN[-token_dl:]}')
+if SEASONALBOT_TOKEN:
+ token_dl = len(SEASONALBOT_TOKEN) // 8
+ log.info(f'Bot token loaded: {SEASONALBOT_TOKEN[:token_dl]}...{SEASONALBOT_TOKEN[-token_dl:]}')
else:
- log.error(f'Bot token not found: {HACKTOBERBOT_TOKEN}')
+ log.error(f'Bot token not found: {SEASONALBOT_TOKEN}')
ghost_unicode = "\N{GHOST}"
bot = commands.Bot(command_prefix=commands.when_mentioned_or(".", f"{ghost_unicode} ", ghost_unicode))
-log.info('Start loading extensions from ./bot/cogs/')
+log.info('Start loading extensions from ./bot/cogs/halloween/')
if __name__ == '__main__':
# Scan for files in the /cogs/ directory and make a list of the file names.
- cogs = [file.stem for file in Path('bot', 'cogs').glob('*.py')]
+ cogs = [file.stem for file in Path('bot', 'cogs', 'hacktober').glob('*.py') if not file.stem.startswith("__")]
for extension in cogs:
try:
- bot.load_extension(f'bot.cogs.{extension}')
+ bot.load_extension(f'bot.cogs.hacktober.{extension}')
log.info(f'Successfully loaded extension: {extension}')
except Exception as e:
log.error(f'Failed to load extension {extension}: {repr(e)} {format_exc()}')
- # print(f'Failed to load extension {extension}.', file=stderr)
- # print_exc()
-log.info(f'Spooky Launch Sequence Initiated...')
-
-bot.run(HACKTOBERBOT_TOKEN)
-
-log.info(f'HackBot has been slain!')
+bot.run(SEASONALBOT_TOKEN)
diff --git a/bot/cogs/__init__.py b/bot/cogs/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/cogs/__init__.py
diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py
new file mode 100644
index 00000000..79780251
--- /dev/null
+++ b/bot/cogs/error_handler.py
@@ -0,0 +1,106 @@
+import logging
+import math
+import sys
+import traceback
+
+from discord.ext import commands
+
+
+class CommandErrorHandler:
+ """A error handler for the PythonDiscord server!"""
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ async def on_command_error(self, ctx, error):
+ """Activates when a command opens an error"""
+
+ if hasattr(ctx.command, 'on_error'):
+ return logging.debug(
+ "A command error occured but " +
+ "the command had it's own error handler"
+ )
+ error = getattr(error, 'original', error)
+ if isinstance(error, commands.CommandNotFound):
+ return logging.debug(
+ f"{ctx.author} called '{ctx.message.content}' " +
+ "but no command was found"
+ )
+ if isinstance(error, commands.UserInputError):
+ logging.debug(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "but entered invalid input!"
+ )
+ return await ctx.send(
+ ":no_entry: The command you specified failed to run." +
+ "This is because the arguments you provided were invalid."
+ )
+ if isinstance(error, commands.CommandOnCooldown):
+ logging.debug(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "but they were on cooldown!"
+ )
+ return await ctx.send(
+ "This command is on cooldown," +
+ f" please retry in {math.ceil(error.retry_after)}s."
+ )
+ if isinstance(error, commands.DisabledCommand):
+ logging.debug(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "but the command was disabled!"
+ )
+ return await ctx.send(
+ ":no_entry: This command has been disabled."
+ )
+ if isinstance(error, commands.NoPrivateMessage):
+ logging.debug(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "in a private message however the command was guild only!"
+ )
+ return await ctx.author.send(
+ ":no_entry: This command can only be used inside a server."
+ )
+ if isinstance(error, commands.BadArgument):
+ if ctx.command.qualified_name == 'tag list':
+ logging.debug(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "but entered an invalid user!"
+ )
+ return await ctx.send(
+ "I could not find that member. Please try again."
+ )
+ else:
+ logging.debug(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "but entered a bad argument!"
+ )
+ return await ctx.send(
+ "The argument you provided was invalid."
+ )
+ if isinstance(error, commands.CheckFailure):
+ logging.debug(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "but the checks failed!"
+ )
+ return await ctx.send(
+ ":no_entry: You are not authorized to use this command."
+ )
+ print(
+ f"Ignoring exception in command {ctx.command}:",
+ file=sys.stderr
+ )
+ logging.warning(
+ f"{ctx.author} called the command '{ctx.command}' " +
+ "however the command failed to run with the error:" +
+ f"-------------\n{error}"
+ )
+ traceback.print_exception(
+ type(error),
+ error,
+ error.__traceback__,
+ file=sys.stderr
+ )
+
+
+def setup(bot):
+ bot.add_cog(CommandErrorHandler(bot))
diff --git a/bot/cogs/evergreen/__init__.py b/bot/cogs/evergreen/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/cogs/evergreen/__init__.py
diff --git a/bot/cogs/evergreen/uptime.py b/bot/cogs/evergreen/uptime.py
new file mode 100644
index 00000000..ec4a3083
--- /dev/null
+++ b/bot/cogs/evergreen/uptime.py
@@ -0,0 +1,33 @@
+import arrow
+from dateutil.relativedelta import relativedelta
+from discord.ext import commands
+
+from bot import start_time
+
+
+class Uptime:
+ """
+ A cog for posting the bots uptime.
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ @commands.command(name='uptime')
+ async def uptime(self, ctx):
+ """
+ Returns the uptime of the bot.
+ """
+ difference = relativedelta(start_time - arrow.utcnow())
+ uptime_string = start_time.shift(
+ seconds=-difference.seconds,
+ minutes=-difference.minutes,
+ hours=-difference.hours,
+ days=-difference.days
+ ).humanize()
+ await ctx.send(f"I started up {uptime_string}.")
+
+
+# Required in order to load the cog, use the class name in the add_cog function.
+def setup(bot):
+ bot.add_cog(Uptime(bot))
diff --git a/bot/cogs/hacktober/__init__.py b/bot/cogs/hacktober/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/cogs/hacktober/__init__.py
diff --git a/bot/cogs/hacktober/candy_collection.py b/bot/cogs/hacktober/candy_collection.py
new file mode 100644
index 00000000..f5f17abb
--- /dev/null
+++ b/bot/cogs/hacktober/candy_collection.py
@@ -0,0 +1,229 @@
+import functools
+import json
+import os
+import random
+
+import discord
+from discord.ext import commands
+
+from bot.constants import HACKTOBER_CHANNEL_ID
+
+json_location = os.path.join("bot", "resources", "halloween", "candy_collection.json")
+
+# chance is 1 in x range, so 1 in 20 range would give 5% chance (for add candy)
+ADD_CANDY_REACTION_CHANCE = 20 # 5%
+ADD_CANDY_EXISTING_REACTION_CHANCE = 10 # 10%
+ADD_SKULL_REACTION_CHANCE = 50 # 2%
+ADD_SKULL_EXISTING_REACTION_CHANCE = 20 # 5%
+
+
+class CandyCollection:
+ def __init__(self, bot):
+ self.bot = bot
+ with open(json_location) as candy:
+ self.candy_json = json.load(candy)
+ self.msg_reacted = self.candy_json['msg_reacted']
+ self.get_candyinfo = dict()
+ for userinfo in self.candy_json['records']:
+ userid = userinfo['userid']
+ self.get_candyinfo[userid] = userinfo
+
+ async def on_message(self, message):
+ """
+ Randomly adds candy or skull to certain messages
+ """
+
+ # make sure its a human message
+ if message.author.bot:
+ return
+ # ensure it's hacktober channel
+ if message.channel.id != HACKTOBER_CHANNEL_ID:
+ return
+
+ # do random check for skull first as it has the lower chance
+ if random.randint(1, ADD_SKULL_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{SKULL}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{SKULL}')
+ # check for the candy chance next
+ if random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{CANDY}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{CANDY}')
+
+ async def on_reaction_add(self, reaction, user):
+ """
+ Add/remove candies from a person if the reaction satisfies criteria
+ """
+
+ message = reaction.message
+ # check to ensure the reactor is human
+ if user.bot:
+ return
+ # check to ensure it is in correct channel
+ if message.channel.id != HACKTOBER_CHANNEL_ID:
+ return
+
+ # if its not a candy or skull, and it is one of 10 most recent messages,
+ # proceed to add a skull/candy with higher chance
+ if str(reaction.emoji) not in ('\N{SKULL}', '\N{CANDY}'):
+ if message.id in await self.ten_recent_msg():
+ await self.reacted_msg_chance(message)
+ return
+
+ for react in self.msg_reacted:
+ # check to see if the message id of a message we added a
+ # reaction to is in json file, and if nobody has won/claimed it yet
+ if react['msg_id'] == message.id and react['won'] is False:
+ react['user_reacted'] = user.id
+ react['won'] = True
+ try:
+ # if they have record/candies in json already it will do this
+ user_records = self.get_candyinfo[user.id]
+ if str(reaction.emoji) == '\N{CANDY}':
+ user_records['record'] += 1
+ if str(reaction.emoji) == '\N{SKULL}':
+ if user_records['record'] <= 3:
+ user_records['record'] = 0
+ lost = 'all of your'
+ else:
+ lost = random.randint(1, 3)
+ user_records['record'] -= lost
+ await self.send_spook_msg(message.author, message.channel, lost)
+
+ except KeyError:
+ # otherwise it will raise KeyError so we need to add them to file
+ if str(reaction.emoji) == '\N{CANDY}':
+ print('ok')
+ d = {"userid": user.id, "record": 1}
+ self.candy_json['records'].append(d)
+ await self.remove_reactions(reaction)
+
+ async def reacted_msg_chance(self, message):
+ """
+ Randomly add a skull or candy to a message if there is a reaction there already
+ (higher probability)
+ """
+
+ if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{SKULL}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{SKULL}')
+
+ if random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
+ d = {"reaction": '\N{CANDY}', "msg_id": message.id, "won": False}
+ self.msg_reacted.append(d)
+ return await message.add_reaction('\N{CANDY}')
+
+ async def ten_recent_msg(self):
+ """Get the last 10 messages sent in the channel"""
+ ten_recent = []
+ recent_msg = max(message.id for message
+ in self.bot._connection._messages
+ if message.channel.id == self.HACKTOBER_CHANNEL_ID)
+
+ channel = await self.hacktober_channel()
+ ten_recent.append(recent_msg.id)
+
+ for i in range(9):
+ o = discord.Object(id=recent_msg.id + i)
+ msg = await next(channel.history(limit=1, before=o))
+ ten_recent.append(msg.id)
+
+ return ten_recent
+
+ async def get_message(self, msg_id):
+ """
+ Get the message from it's ID.
+ """
+
+ try:
+ o = discord.Object(id=msg_id + 1)
+ # Use history rather than get_message due to
+ # poor ratelimit (50/1s vs 1/1s)
+ msg = await next(self.hacktober_channel.history(limit=1, before=o))
+
+ if msg.id != msg_id:
+ return None
+
+ return msg
+
+ except Exception:
+ return None
+
+ async def hacktober_channel(self):
+ """
+ Get #hacktoberbot channel from it's id
+ """
+ return self.bot.get_channel(id=HACKTOBER_CHANNEL_ID)
+
+ async def remove_reactions(self, reaction):
+ """
+ Remove all candy/skull reactions
+ """
+
+ try:
+ async for user in reaction.users():
+ await reaction.message.remove_reaction(reaction.emoji, user)
+
+ except discord.HTTPException:
+ pass
+
+ async def send_spook_msg(self, author, channel, candies):
+ """
+ Send a spooky message
+ """
+ e = discord.Embed(colour=author.colour)
+ e.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; "
+ f"I took {candies} candies and quickly took flight.")
+ await channel.send(embed=e)
+
+ def save_to_json(self):
+ """
+ Save json to the file.
+ """
+ with open(json_location, 'w') as outfile:
+ json.dump(self.candy_json, outfile)
+
+ @commands.command()
+ async def candy(self, ctx):
+ """
+ Get the candy leaderboard and save to json when this is called
+ """
+
+ # use run_in_executor to prevent blocking
+ thing = functools.partial(self.save_to_json)
+ await self.bot.loop.run_in_executor(None, thing)
+
+ emoji = (
+ '\N{FIRST PLACE MEDAL}',
+ '\N{SECOND PLACE MEDAL}',
+ '\N{THIRD PLACE MEDAL}',
+ '\N{SPORTS MEDAL}',
+ '\N{SPORTS MEDAL}'
+ )
+
+ top_sorted = sorted(self.candy_json['records'], key=lambda k: k.get('record', 0), reverse=True)
+ top_five = top_sorted[:5]
+
+ usersid = []
+ records = []
+ for record in top_five:
+ usersid.append(record['userid'])
+ records.append(record['record'])
+
+ value = '\n'.join(f'{emoji[index]} <@{usersid[index]}>: {records[index]}'
+ for index in range(0, len(usersid))) or 'No Candies'
+
+ e = discord.Embed(colour=discord.Colour.blurple())
+ e.add_field(name="Top Candy Records", value=value, inline=False)
+ e.add_field(name='\u200b',
+ value=f"Candies will randomly appear on messages sent. "
+ f"\nHit the candy when it appears as fast as possible to get the candy! "
+ f"\nBut beware the ghosts...",
+ inline=False)
+ await ctx.send(embed=e)
+
+
+def setup(bot):
+ bot.add_cog(CandyCollection(bot))
diff --git a/bot/cogs/hacktober/hacktoberstats.py b/bot/cogs/hacktober/hacktoberstats.py
new file mode 100644
index 00000000..0755503c
--- /dev/null
+++ b/bot/cogs/hacktober/hacktoberstats.py
@@ -0,0 +1,326 @@
+import json
+import logging
+import re
+import typing
+from collections import Counter
+from datetime import datetime
+from pathlib import Path
+
+import aiohttp
+import discord
+from discord.ext import commands
+
+
+class Stats:
+ def __init__(self, bot):
+ self.bot = bot
+ self.link_json = Path('./bot/resources', 'github_links.json')
+ self.linked_accounts = self.load_linked_users()
+
+ @commands.group(
+ name='stats',
+ aliases=('hacktoberstats', 'getstats', 'userstats'),
+ invoke_without_command=True
+ )
+ async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None):
+ """
+ If invoked without a subcommand or github_username, get the invoking user's stats if
+ they've linked their Discord name to GitHub using .stats link
+
+ If invoked with a github_username, get that user's contributions
+ """
+ if not github_username:
+ author_id, author_mention = Stats._author_mention_from_context(ctx)
+
+ if str(author_id) in self.linked_accounts.keys():
+ github_username = self.linked_accounts[author_id]["github_username"]
+ logging.info(f"Getting stats for {author_id} linked GitHub account '{github_username}'")
+ else:
+ msg = (
+ f"{author_mention}, you have not linked a GitHub account\n\n"
+ f"You can link your GitHub account using:\n```{ctx.prefix}stats link github_username```\n"
+ f"Or query GitHub stats directly using:\n```{ctx.prefix}stats github_username```"
+ )
+ await ctx.send(msg)
+ return
+
+ await self.get_stats(ctx, github_username)
+
+ @hacktoberstats_group.command(name="link")
+ async def link_user(self, ctx: commands.Context, github_username: str = None):
+ """
+ Link the invoking user's Github github_username to their Discord ID
+
+ Linked users are stored as a nested dict:
+ {
+ Discord_ID: {
+ "github_username": str
+ "date_added": datetime
+ }
+ }
+ """
+ author_id, author_mention = Stats._author_mention_from_context(ctx)
+ if github_username:
+ if str(author_id) in self.linked_accounts.keys():
+ old_username = self.linked_accounts[author_id]["github_username"]
+ logging.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")
+ await ctx.send(f"{author_mention}, your GitHub username has been updated to: '{github_username}'")
+ else:
+ logging.info(f"{author_id} has added a github link to '{github_username}'")
+ await ctx.send(f"{author_mention}, your GitHub username has been added")
+
+ self.linked_accounts[author_id] = {
+ "github_username": github_username,
+ "date_added": datetime.now()
+ }
+
+ self.save_linked_users()
+ else:
+ logging.info(f"{author_id} tried to link a GitHub account but didn't provide a username")
+ await ctx.send(f"{author_mention}, a GitHub username is required to link your account")
+
+ @hacktoberstats_group.command(name="unlink")
+ async def unlink_user(self, ctx: commands.Context):
+ """
+ Remove the invoking user's account link from the log
+ """
+ author_id, author_mention = Stats._author_mention_from_context(ctx)
+
+ stored_user = self.linked_accounts.pop(author_id, None)
+ if stored_user:
+ await ctx.send(f"{author_mention}, your GitHub profile has been unlinked")
+ logging.info(f"{author_id} has unlinked their GitHub account")
+ else:
+ await ctx.send(f"{author_mention}, you do not currently have a linked GitHub account")
+ logging.info(f"{author_id} tried to unlink their GitHub account but no account was linked")
+
+ self.save_linked_users()
+
+ def load_linked_users(self) -> typing.Dict:
+ """
+ Load list of linked users from local JSON file
+
+ Linked users are stored as a nested dict:
+ {
+ Discord_ID: {
+ "github_username": str
+ "date_added": datetime
+ }
+ }
+ """
+ if self.link_json.exists():
+ logging.info(f"Loading linked GitHub accounts from '{self.link_json}'")
+ with open(self.link_json, 'r') as fID:
+ linked_accounts = json.load(fID)
+
+ logging.info(f"Loaded {len(linked_accounts)} linked GitHub accounts from '{self.link_json}'")
+ return linked_accounts
+ else:
+ logging.info(f"Linked account log: '{self.link_json}' does not exist")
+ return {}
+
+ def save_linked_users(self):
+ """
+ Save list of linked users to local JSON file
+
+ Linked users are stored as a nested dict:
+ {
+ Discord_ID: {
+ "github_username": str
+ "date_added": datetime
+ }
+ }
+ """
+ logging.info(f"Saving linked_accounts to '{self.link_json}'")
+ with open(self.link_json, 'w') as fID:
+ json.dump(self.linked_accounts, fID, default=str)
+ logging.info(f"linked_accounts saved to '{self.link_json}'")
+
+ async def get_stats(self, ctx: commands.Context, github_username: str):
+ """
+ Query GitHub's API for PRs created by a GitHub user during the month of October that
+ do not have an 'invalid' tag
+
+ For example:
+ !getstats heavysaturn
+
+ If a valid github_username is provided, an embed is generated and posted to the channel
+
+ Otherwise, post a helpful error message
+ """
+ prs = await self.get_october_prs(github_username)
+
+ if prs:
+ stats_embed = self.build_embed(github_username, prs)
+ await ctx.send('Here are some stats!', embed=stats_embed)
+ else:
+ await ctx.send(f"No October GitHub contributions found for '{github_username}'")
+
+ def build_embed(self, github_username: str, prs: typing.List[dict]) -> discord.Embed:
+ """
+ Return a stats embed built from github_username's PRs
+ """
+ logging.info(f"Building Hacktoberfest embed for GitHub user: '{github_username}'")
+ pr_stats = self._summarize_prs(prs)
+
+ n = pr_stats['n_prs']
+ if n >= 5:
+ shirtstr = f"**{github_username} has earned a tshirt!**"
+ elif n == 4:
+ shirtstr = f"**{github_username} is 1 PR away from a tshirt!**"
+ else:
+ shirtstr = f"**{github_username} is {5 - n} PRs away from a tshirt!**"
+
+ stats_embed = discord.Embed(
+ title=f"{github_username}'s Hacktoberfest",
+ color=discord.Color(0x9c4af7),
+ description=f"{github_username} has made {n} {Stats._contributionator(n)} in October\n\n{shirtstr}\n\n"
+ )
+
+ stats_embed.set_thumbnail(url=f"https://www.github.com/{github_username}.png")
+ stats_embed.set_author(
+ name="Hacktoberfest",
+ url="https://hacktoberfest.digitalocean.com",
+ icon_url="https://hacktoberfest.digitalocean.com/assets/logo-hacktoberfest.png"
+ )
+ stats_embed.add_field(
+ name="Top 5 Repositories:",
+ value=self._build_top5str(pr_stats)
+ )
+
+ logging.info(f"Hacktoberfest PR built for GitHub user '{github_username}'")
+ return stats_embed
+
+ @staticmethod
+ async def get_october_prs(github_username: str) -> typing.List[dict]:
+ """
+ Query GitHub's API for PRs created during the month of October by github_username
+ that do not have an 'invalid' tag
+
+ If PRs are found, return a list of dicts with basic PR information
+
+ For each PR:
+ {
+ "repo_url": str
+ "repo_shortname": str (e.g. "python-discord/seasonalbot")
+ "created_at": datetime.datetime
+ }
+
+ Otherwise, return None
+ """
+ logging.info(f"Generating Hacktoberfest PR query for GitHub user: '{github_username}'")
+ base_url = "https://api.github.com/search/issues?q="
+ not_label = "invalid"
+ action_type = "pr"
+ is_query = f"public+author:{github_username}"
+ date_range = "2018-10-01..2018-10-31"
+ per_page = "300"
+ query_url = (
+ f"{base_url}"
+ f"-label:{not_label}"
+ f"+type:{action_type}"
+ f"+is:{is_query}"
+ f"+created:{date_range}"
+ f"&per_page={per_page}"
+ )
+
+ headers = {"user-agent": "Discord Python Hactoberbot"}
+ async with aiohttp.ClientSession() as session:
+ async with session.get(query_url, headers=headers) as resp:
+ jsonresp = await resp.json()
+
+ if "message" in jsonresp.keys():
+ # One of the parameters is invalid, short circuit for now
+ api_message = jsonresp["errors"][0]["message"]
+ logging.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")
+ return
+ else:
+ if jsonresp["total_count"] == 0:
+ # Short circuit if there aren't any PRs
+ logging.info(f"No Hacktoberfest PRs found for GitHub user: '{github_username}'")
+ return
+ else:
+ logging.info(f"Found {len(jsonresp['items'])} Hacktoberfest PRs for GitHub user: '{github_username}'")
+ outlist = []
+ for item in jsonresp["items"]:
+ shortname = Stats._get_shortname(item["repository_url"])
+ itemdict = {
+ "repo_url": f"https://www.github.com/{shortname}",
+ "repo_shortname": shortname,
+ "created_at": datetime.strptime(
+ item["created_at"], r"%Y-%m-%dT%H:%M:%SZ"
+ ),
+ }
+ outlist.append(itemdict)
+ return outlist
+
+ @staticmethod
+ def _get_shortname(in_url: str) -> str:
+ """
+ Extract shortname from https://api.github.com/repos/* URL
+
+ e.g. "https://api.github.com/repos/python-discord/seasonalbot"
+ |
+ V
+ "python-discord/seasonalbot"
+ """
+ exp = r"https?:\/\/api.github.com\/repos\/([/\-\_\.\w]+)"
+ return re.findall(exp, in_url)[0]
+
+ @staticmethod
+ def _summarize_prs(prs: typing.List[dict]) -> typing.Dict:
+ """
+ Generate statistics from an input list of PR dictionaries, as output by get_october_prs
+
+ Return a dictionary containing:
+ {
+ "n_prs": int
+ "top5": [(repo_shortname, ncontributions), ...]
+ }
+ """
+ contributed_repos = [pr["repo_shortname"] for pr in prs]
+ return {"n_prs": len(prs), "top5": Counter(contributed_repos).most_common(5)}
+
+ @staticmethod
+ def _build_top5str(stats: typing.List[tuple]) -> str:
+ """
+ Build a string from the Top 5 contributions that is compatible with a discord.Embed field
+
+ Top 5 contributions should be a list of tuples, as output in the stats dictionary by
+ _summarize_prs
+
+ String is of the form:
+ n contribution(s) to [shortname](url)
+ ...
+ """
+ baseURL = "https://www.github.com/"
+ contributionstrs = []
+ for repo in stats['top5']:
+ n = repo[1]
+ contributionstrs.append(f"{n} {Stats._contributionator(n)} to [{repo[0]}]({baseURL}{repo[0]})")
+
+ return "\n".join(contributionstrs)
+
+ @staticmethod
+ def _contributionator(n: int) -> str:
+ """
+ Return "contribution" or "contributions" based on the value of n
+ """
+ if n == 1:
+ return "contribution"
+ else:
+ return "contributions"
+
+ @staticmethod
+ def _author_mention_from_context(ctx: commands.Context) -> typing.Tuple:
+ """
+ Return stringified Message author ID and mentionable string from commands.Context
+ """
+ author_id = str(ctx.message.author.id)
+ author_mention = ctx.message.author.mention
+
+ return author_id, author_mention
+
+
+def setup(bot):
+ bot.add_cog(Stats(bot))
diff --git a/bot/cogs/halloween_facts.py b/bot/cogs/hacktober/halloween_facts.py
index e97c80d2..bd164e30 100644
--- a/bot/cogs/halloween_facts.py
+++ b/bot/cogs/hacktober/halloween_facts.py
@@ -7,6 +7,8 @@ from pathlib import Path
import discord
from discord.ext import commands
+from bot.constants import HACKTOBER_CHANNEL_ID
+
SPOOKY_EMOJIS = [
"\N{BAT}",
"\N{DERELICT HOUSE BUILDING}",
@@ -18,7 +20,6 @@ SPOOKY_EMOJIS = [
"\N{SPIDER WEB}",
]
PUMPKIN_ORANGE = discord.Color(0xFF7518)
-HACKTOBERBOT_CHANNEL_ID = 498804484324196362
INTERVAL = timedelta(hours=6).total_seconds()
@@ -26,13 +27,13 @@ class HalloweenFacts:
def __init__(self, bot):
self.bot = bot
- with open(Path("./bot/resources", "halloween_facts.json"), "r") as file:
+ with open(Path("bot", "resources", "halloween", "halloween_facts.json"), "r") as file:
self.halloween_facts = json.load(file)
self.channel = None
self.last_fact = None
async def on_ready(self):
- self.channel = self.bot.get_channel(HACKTOBERBOT_CHANNEL_ID)
+ self.channel = self.bot.get_channel(HACKTOBER_CHANNEL_ID)
self.bot.loop.create_task(self._fact_publisher_task())
async def _fact_publisher_task(self):
diff --git a/bot/cogs/halloweenify.py b/bot/cogs/hacktober/halloweenify.py
index a5fe45ef..9b93ac99 100644
--- a/bot/cogs/halloweenify.py
+++ b/bot/cogs/hacktober/halloweenify.py
@@ -21,7 +21,7 @@ class Halloweenify:
"""
Change your nickname into a much spookier one!
"""
- with open(Path('./bot/resources', 'halloweenify.json'), 'r') as f:
+ with open(Path('bot', 'resources', 'halloween', 'halloweenify.json'), 'r') as f:
data = load(f)
# Choose a random character from our list we loaded above and set apart the nickname and image url.
diff --git a/bot/cogs/hacktober/monstersurvey.py b/bot/cogs/hacktober/monstersurvey.py
new file mode 100644
index 00000000..45587fe1
--- /dev/null
+++ b/bot/cogs/hacktober/monstersurvey.py
@@ -0,0 +1,191 @@
+import json
+import logging
+import os
+
+from discord import Embed
+from discord.ext import commands
+from discord.ext.commands import Bot, Context
+
+log = logging.getLogger(__name__)
+
+EMOJIS = {
+ 'SUCCESS': u'\u2705',
+ 'ERROR': u'\u274C'
+}
+
+
+class MonsterSurvey:
+ """
+ Vote for your favorite monster!
+ This command allows users to vote for their favorite listed monster.
+ Users may change their vote, but only their current vote will be counted.
+ """
+
+ def __init__(self, bot: Bot):
+ """Initializes values for the bot to use within the voting commands."""
+ self.bot = bot
+ self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json')
+ with open(self.registry_location, 'r') as jason:
+ self.voter_registry = json.load(jason)
+
+ def json_write(self):
+ log.info("Saved Monster Survey Results")
+ with open(self.registry_location, 'w') as jason:
+ json.dump(self.voter_registry, jason, indent=2)
+
+ def cast_vote(self, id: int, monster: str):
+ """
+
+ :param id: The id of the person voting
+ :param monster: the string key of the json that represents a monster
+ :return: None
+ """
+ vr = self.voter_registry
+ for m in vr.keys():
+ if id not in vr[m]['votes'] and m == monster:
+ vr[m]['votes'].append(id)
+ else:
+ if id in vr[m]['votes'] and m != monster:
+ vr[m]['votes'].remove(id)
+
+ def get_name_by_leaderboard_index(self, n):
+ n = n - 1
+ vr = self.voter_registry
+ top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
+ name = top[n] if n >= 0 else None
+ return name
+
+ @commands.group(
+ name='monster',
+ aliases=['ms']
+ )
+ async def monster_group(self, ctx: Context):
+ """
+ The base voting command. If nothing is called, then it will return an embed.
+ """
+
+ if ctx.invoked_subcommand is None:
+ default_embed = Embed(
+ title='Monster Voting',
+ color=0xFF6800,
+ description='Vote for your favorite monster!'
+ )
+ default_embed.add_field(
+ name='.monster show monster_name(optional)',
+ value='Show a specific monster. If none is listed, it will give you an error with valid choices.',
+ inline=False)
+ default_embed.add_field(
+ name='.monster vote monster_name',
+ value='Vote for a specific monster. You get one vote, but can change it at any time.',
+ inline=False
+ )
+ default_embed.add_field(
+ name='.monster leaderboard',
+ value='Which monster has the most votes? This command will tell you.',
+ inline=False
+ )
+ default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry.keys())}")
+ return await ctx.send(embed=default_embed)
+
+ @monster_group.command(
+ name='vote'
+ )
+ async def monster_vote(self, ctx: Context, name=None):
+ """Casts a vote for a particular monster, or displays a list of monsters that can be voted for
+ if one is not given."""
+ if name is None:
+ await ctx.invoke(self.monster_leaderboard)
+ return
+ vote_embed = Embed(
+ name='Monster Voting',
+ color=0xFF6800
+ )
+ if isinstance(name, int):
+ name = self.get_name_by_leaderboard_index(name)
+ else:
+ name = name.lower()
+ m = self.voter_registry.get(name)
+ if m is None:
+
+ vote_embed.description = f'You cannot vote for {name} because it\'s not in the running.'
+ vote_embed.add_field(
+ name='Use `.monster show {monster_name}` for more information on a specific monster',
+ value='or use `.monster vote {monster}` to cast your vote for said monster.',
+ inline=False
+ )
+ vote_embed.add_field(
+ name='You may vote for or show the following monsters:',
+ value=f"{', '.join(self.voter_registry.keys())}"
+ )
+ return await ctx.send(embed=vote_embed)
+ self.cast_vote(ctx.author.id, name)
+ vote_embed.add_field(
+ name='Vote successful!',
+ value=f'You have successfully voted for {m["full_name"]}!',
+ inline=False
+ )
+ vote_embed.set_thumbnail(url=m['image'])
+ vote_embed.set_footer(text="Please note that any previous votes have been removed.")
+ self.json_write()
+ return await ctx.send(embed=vote_embed)
+
+ @monster_group.command(
+ name='show'
+ )
+ async def monster_show(self, ctx: Context, name=None):
+ """
+ Shows the named monster. If one is not named, it sends the default voting embed instead.
+ :param ctx:
+ :param name:
+ :return:
+ """
+ if name is None:
+ await ctx.invoke(self.monster_leaderboard)
+ return
+ if isinstance(name, int):
+ m = self.voter_registry.get(self.get_name_by_leaderboard_index(name))
+ else:
+ name = name.lower()
+ m = self.voter_registry.get(name)
+ if not m:
+ await ctx.send('That monster does not exist.')
+ return await ctx.invoke(self.monster_vote)
+ embed = Embed(title=m['full_name'], color=0xFF6800)
+ embed.add_field(name='Summary', value=m['summary'])
+ embed.set_image(url=m['image'])
+ embed.set_footer(text=f'To vote for this monster, type .monster vote {name}')
+ return await ctx.send(embed=embed)
+
+ @monster_group.command(
+ name='leaderboard',
+ aliases=['lb']
+ )
+ async def monster_leaderboard(self, ctx: Context):
+ """
+ Shows the current standings.
+ :param ctx:
+ :return:
+ """
+ vr = self.voter_registry
+ top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
+
+ embed = Embed(title="Monster Survey Leader Board", color=0xFF6800)
+ total_votes = sum(len(m['votes']) for m in self.voter_registry.values())
+ for rank, m in enumerate(top):
+ votes = len(vr[m]['votes'])
+ percentage = ((votes / total_votes) * 100) if total_votes > 0 else 0
+ embed.add_field(name=f"{rank+1}. {vr[m]['full_name']}",
+ value=f"{votes} votes. {percentage:.1f}% of total votes.\n"
+ f"Vote for this monster by typing "
+ f"'.monster vote {m}'\n"
+ f"Get more information on this monster by typing "
+ f"'.monster show {m}'",
+ inline=False)
+
+ embed.set_footer(text="You can also vote by their rank number. '.monster vote {number}' ")
+ await ctx.send(embed=embed)
+
+
+def setup(bot):
+ bot.add_cog(MonsterSurvey(bot))
+ log.debug("MonsterSurvey COG Loaded")
diff --git a/bot/cogs/movie.py b/bot/cogs/hacktober/movie.py
index 925f813f..925f813f 100644
--- a/bot/cogs/movie.py
+++ b/bot/cogs/hacktober/movie.py
diff --git a/bot/cogs/hacktober/spookyavatar.py b/bot/cogs/hacktober/spookyavatar.py
new file mode 100644
index 00000000..ad8a9242
--- /dev/null
+++ b/bot/cogs/hacktober/spookyavatar.py
@@ -0,0 +1,52 @@
+import os
+from io import BytesIO
+
+import aiohttp
+import discord
+from discord.ext import commands
+from PIL import Image
+
+from bot.utils import spookifications
+
+
+class SpookyAvatar:
+
+ """
+ A cog that spookifies an avatar.
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ async def get(self, url):
+ """
+ Returns the contents of the supplied url.
+ """
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url) as resp:
+ return await resp.read()
+
+ @commands.command(name='savatar', aliases=['spookyavatar', 'spookify'],
+ brief='Spookify an user\'s avatar.')
+ async def spooky_avatar(self, ctx, user: discord.Member = None):
+ """
+ A command to print the user's spookified avatar.
+ """
+ if user is None:
+ user = ctx.message.author
+
+ embed = discord.Embed(colour=0xFF0000)
+ embed.title = "Is this you or am I just really paranoid?"
+ embed.set_author(name=str(user.name), icon_url=user.avatar_url)
+ resp = await self.get(user.avatar_url)
+ im = Image.open(BytesIO(resp))
+ modified_im = spookifications.get_random_effect(im)
+ modified_im.save(str(ctx.message.id)+'.png')
+ f = discord.File(str(ctx.message.id)+'.png')
+ embed.set_image(url='attachment://'+str(ctx.message.id)+'.png')
+ await ctx.send(file=f, embed=embed)
+ os.remove(str(ctx.message.id)+'.png')
+
+
+def setup(bot):
+ bot.add_cog(SpookyAvatar(bot))
diff --git a/bot/cogs/hacktober/spookyreact.py b/bot/cogs/hacktober/spookyreact.py
new file mode 100644
index 00000000..8e9e8db6
--- /dev/null
+++ b/bot/cogs/hacktober/spookyreact.py
@@ -0,0 +1,69 @@
+import logging
+import re
+
+import discord
+
+SPOOKY_TRIGGERS = {
+ 'spooky': (r"\bspo{2,}ky\b", "\U0001F47B"),
+ 'skeleton': (r"\bskeleton\b", "\U0001F480"),
+ 'doot': (r"\bdo{2,}t\b", "\U0001F480"),
+ 'pumpkin': (r"\bpumpkin\b", "\U0001F383"),
+ 'halloween': (r"\bhalloween\b", "\U0001F383"),
+ 'jack-o-lantern': (r"\bjack-o-lantern\b", "\U0001F383"),
+ 'danger': (r"\bdanger\b", "\U00002620")
+}
+
+
+class SpookyReact:
+
+ """
+ A cog that makes the bot react to message triggers.
+ """
+
+ def __init__(self, bot):
+ self.bot = bot
+
+ async def on_message(self, ctx: discord.Message):
+ """
+ A command to send the seasonalbot github project
+
+ Lines that begin with the bot's command prefix are ignored
+
+ Seasonalbot's own messages are ignored
+ """
+ for trigger in SPOOKY_TRIGGERS.keys():
+ trigger_test = re.search(SPOOKY_TRIGGERS[trigger][0], ctx.content.lower())
+ if trigger_test:
+ # Check message for bot replies and/or command invocations
+ # Short circuit if they're found, logging is handled in _short_circuit_check
+ if await self._short_circuit_check(ctx):
+ return
+ else:
+ await ctx.add_reaction(SPOOKY_TRIGGERS[trigger][1])
+ logging.info(f"Added '{trigger}' reaction to message ID: {ctx.id}")
+
+ async def _short_circuit_check(self, ctx: discord.Message) -> bool:
+ """
+ Short-circuit helper check.
+
+ Return True if:
+ * author is the bot
+ * prefix is not None
+ """
+ # Check for self reaction
+ if ctx.author == self.bot.user:
+ logging.info(f"Ignoring reactions on self message. Message ID: {ctx.id}")
+ return True
+
+ # Check for command invocation
+ # Because on_message doesn't give a full Context object, generate one first
+ tmp_ctx = await self.bot.get_context(ctx)
+ if tmp_ctx.prefix:
+ logging.info(f"Ignoring reactions on command invocation. Message ID: {ctx.id}")
+ return True
+
+ return False
+
+
+def setup(bot):
+ bot.add_cog(SpookyReact(bot))
diff --git a/bot/cogs/spookysound.py b/bot/cogs/hacktober/spookysound.py
index dd607097..e1598517 100644
--- a/bot/cogs/spookysound.py
+++ b/bot/cogs/hacktober/spookysound.py
@@ -4,7 +4,7 @@ from pathlib import Path
import discord
from discord.ext import commands
-HACKTOBERBOT_VOICE_CHANNEL_ID = 498804789287714816
+from bot.constants import HACKTOBER_VOICE_CHANNEL_ID
class SpookySound:
@@ -17,16 +17,17 @@ class SpookySound:
self.sound_files = list(Path("./bot/resources/spookysounds").glob("*.mp3"))
self.channel = None
- async def on_ready(self):
- self.channel = self.bot.get_channel(HACKTOBERBOT_VOICE_CHANNEL_ID)
-
- @commands.cooldown(rate=1, per=120)
+ @commands.cooldown(rate=1, per=1)
@commands.command(brief="Play a spooky sound, restricted to once per 2 mins")
async def spookysound(self, ctx):
"""
Connect to the Hacktoberbot voice channel, play a random spooky sound, then disconnect. Cannot be used more than
once in 2 minutes.
"""
+ if not self.channel:
+ await self.bot.wait_until_ready()
+ self.channel = self.bot.get_channel(HACKTOBER_VOICE_CHANNEL_ID)
+
await ctx.send("Initiating spooky sound...")
file_path = random.choice(self.sound_files)
src = discord.FFmpegPCMAudio(str(file_path.resolve()))
diff --git a/bot/cogs/hacktoberstats.py b/bot/cogs/hacktoberstats.py
deleted file mode 100644
index ac81b887..00000000
--- a/bot/cogs/hacktoberstats.py
+++ /dev/null
@@ -1,193 +0,0 @@
-import re
-import typing
-from collections import Counter
-from datetime import datetime
-
-import aiohttp
-import discord
-from discord.ext import commands
-
-
-class Stats:
- def __init__(self, bot):
- self.bot = bot
-
- @commands.command(
- name="stats",
- aliases=["getstats", "userstats"],
- brief="Get a user's Hacktoberfest contribution stats",
- )
- async def get_stats(self, ctx, username: str):
- """
- Query GitHub's API for PRs created by a GitHub user during the month of October that
- do not have an 'invalid' tag
-
- For example:
- !getstats heavysaturn
-
- If a valid username is provided, an embed is generated and posted to the channel
-
- Otherwise, post a helpful error message
-
- The first input argument is treated as the username, any additional inputs are discarded
- """
- prs = await self.get_october_prs(username)
-
- if prs:
- stats_embed = self.build_embed(username, prs)
- await ctx.send('Here are some stats!', embed=stats_embed)
- else:
- await ctx.send(f"No October GitHub contributions found for '{username}'")
-
- def build_embed(self, username: str, prs: typing.List[dict]) -> discord.Embed:
- """
- Return a stats embed built from username's PRs
- """
- pr_stats = self._summarize_prs(prs)
-
- n = pr_stats['n_prs']
- if n >= 5:
- shirtstr = f"**{username} has earned a tshirt!**"
- elif n == 4:
- shirtstr = f"**{username} is 1 PR away from a tshirt!**"
- else:
- shirtstr = f"**{username} is {5 - n} PRs away from a tshirt!**"
-
- stats_embed = discord.Embed(
- title=f"{username}'s Hacktoberfest",
- color=discord.Color(0x9c4af7),
- description=f"{username} has made {n} {Stats._contributionator(n)} in October\n\n{shirtstr}\n\n"
- )
-
- stats_embed.set_thumbnail(url=f"https://www.github.com/{username}.png")
- stats_embed.set_author(
- name="Hacktoberfest",
- url="https://hacktoberfest.digitalocean.com",
- icon_url="https://hacktoberfest.digitalocean.com/assets/logo-hacktoberfest.png"
- )
- stats_embed.add_field(
- name="Top 5 Repositories:",
- value=self._build_top5str(pr_stats)
- )
-
- return stats_embed
-
- @staticmethod
- async def get_october_prs(username: str) -> typing.List[dict]:
- """
- Query GitHub's API for PRs created during the month of October by username that do
- not have an 'invalid' tag
-
- If PRs are found, return a list of dicts with basic PR information
-
- For each PR:
- {
- "repo_url": str
- "repo_shortname": str (e.g. "discord-python/hacktoberbot")
- "created_at": datetime.datetime
- }
-
- Otherwise, return None
- """
- base_url = "https://api.github.com/search/issues?q="
- not_label = "invalid"
- action_type = "pr"
- is_query = f"public+author:{username}"
- date_range = "2018-10-01..2018-10-31"
- per_page = "300"
- query_url = (
- f"{base_url}"
- f"-label:{not_label}"
- f"+type:{action_type}"
- f"+is:{is_query}"
- f"+created:{date_range}"
- f"&per_page={per_page}"
- )
-
- headers = {"user-agent": "Discord Python Hactoberbot"}
- async with aiohttp.ClientSession() as session:
- async with session.get(query_url, headers=headers) as resp:
- jsonresp = await resp.json()
-
- if "message" in jsonresp.keys():
- # One of the parameters is invalid, short circuit for now
- # In the future, log: jsonresp["errors"][0]["message"]
- return
- else:
- if jsonresp["total_count"] == 0:
- # Short circuit if there aren't any PRs
- return
- else:
- outlist = []
- for item in jsonresp["items"]:
- shortname = Stats._get_shortname(item["repository_url"])
- itemdict = {
- "repo_url": f"https://www.github.com/{shortname}",
- "repo_shortname": shortname,
- "created_at": datetime.strptime(
- item["created_at"], r"%Y-%m-%dT%H:%M:%SZ"
- ),
- }
- outlist.append(itemdict)
- return outlist
-
- @staticmethod
- def _get_shortname(in_url: str) -> str:
- """
- Extract shortname from https://api.github.com/repos/* URL
-
- e.g. "https://api.github.com/repos/discord-python/hacktoberbot"
- |
- V
- "discord-python/hacktoberbot"
- """
- exp = r"https?:\/\/api.github.com\/repos\/([/\-\w]+)"
- return re.findall(exp, in_url)[0]
-
- @staticmethod
- def _summarize_prs(prs: typing.List[dict]) -> typing.Dict:
- """
- Generate statistics from an input list of PR dictionaries, as output by get_october_prs
-
- Return a dictionary containing:
- {
- "n_prs": int
- "top5": [(repo_shortname, ncontributions), ...]
- }
- """
- contributed_repos = [pr["repo_shortname"] for pr in prs]
- return {"n_prs": len(prs), "top5": Counter(contributed_repos).most_common(5)}
-
- @staticmethod
- def _build_top5str(stats: typing.List[tuple]) -> str:
- """
- Build a string from the Top 5 contributions that is compatible with a discord.Embed field
-
- Top 5 contributions should be a list of tuples, as output in the stats dictionary by
- _summarize_prs
-
- String is of the form:
- n contribution(s) to [shortname](url)
- ...
- """
- baseURL = "https://www.github.com/"
- contributionstrs = []
- for repo in stats['top5']:
- n = repo[1]
- contributionstrs.append(f"{n} {Stats._contributionator(n)} to [{repo[0]}]({baseURL}{repo[0]})")
-
- return "\n".join(contributionstrs)
-
- @staticmethod
- def _contributionator(n: int) -> str:
- """
- Return "contribution" or "contributions" based on the value of n
- """
- if n == 1:
- return "contribution"
- else:
- return "contributions"
-
-
-def setup(bot):
- bot.add_cog(Stats(bot))
diff --git a/bot/cogs/spookyreact.py b/bot/cogs/spookyreact.py
deleted file mode 100644
index 2652a60e..00000000
--- a/bot/cogs/spookyreact.py
+++ /dev/null
@@ -1,31 +0,0 @@
-SPOOKY_TRIGGERS = {
- 'spooky': "\U0001F47B",
- 'skeleton': "\U0001F480",
- 'doot': "\U0001F480",
- 'pumpkin': "\U0001F383",
- 'halloween': "\U0001F383",
- 'jack-o-lantern': "\U0001F383",
- 'danger': "\U00002620"
-}
-
-
-class SpookyReact:
-
- """
- A cog that makes the bot react to message triggers.
- """
-
- def __init__(self, bot):
- self.bot = bot
-
- async def on_message(self, ctx):
- """
- A command to send the hacktoberbot github project
- """
- for trigger in SPOOKY_TRIGGERS.keys():
- if trigger in ctx.content.lower():
- await ctx.add_reaction(SPOOKY_TRIGGERS[trigger])
-
-
-def setup(bot):
- bot.add_cog(SpookyReact(bot))
diff --git a/bot/cogs/template.py b/bot/cogs/template.py
index aa01432c..e1b646e3 100644
--- a/bot/cogs/template.py
+++ b/bot/cogs/template.py
@@ -13,9 +13,9 @@ class Template:
@commands.command(name='repo', aliases=['repository', 'project'], brief='A link to the repository of this bot.')
async def repository(self, ctx):
"""
- A command to send the hacktoberbot github project
+ A command to send the seasonalbot github project
"""
- await ctx.send('https://github.com/discord-python/hacktoberbot')
+ await ctx.send('https://github.com/python-discord/seasonalbot')
@commands.group(name='git', invoke_without_command=True, brief="A link to resources for learning Git")
async def github(self, ctx):
diff --git a/bot/constants.py b/bot/constants.py
index e69de29b..f2bf04a2 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -0,0 +1,2 @@
+HACKTOBER_CHANNEL_ID = 414574275865870337
+HACKTOBER_VOICE_CHANNEL_ID = 514420006474219521
diff --git a/bot/resources/halloween/bat-clipart.png b/bot/resources/halloween/bat-clipart.png
new file mode 100644
index 00000000..7df26ba9
--- /dev/null
+++ b/bot/resources/halloween/bat-clipart.png
Binary files differ
diff --git a/bot/resources/halloween/bloody-pentagram.png b/bot/resources/halloween/bloody-pentagram.png
new file mode 100644
index 00000000..4e6da07a
--- /dev/null
+++ b/bot/resources/halloween/bloody-pentagram.png
Binary files differ
diff --git a/bot/resources/halloween/candy_collection.json b/bot/resources/halloween/candy_collection.json
new file mode 100644
index 00000000..6313dd10
--- /dev/null
+++ b/bot/resources/halloween/candy_collection.json
@@ -0,0 +1,8 @@
+{
+ "msg_reacted": [
+
+ ],
+ "records": [
+
+ ]
+}
diff --git a/bot/resources/halloween_facts.json b/bot/resources/halloween/halloween_facts.json
index fc6fa85f..fc6fa85f 100644
--- a/bot/resources/halloween_facts.json
+++ b/bot/resources/halloween/halloween_facts.json
diff --git a/bot/resources/halloweenify.json b/bot/resources/halloween/halloweenify.json
index 88c46bfc..88c46bfc 100644
--- a/bot/resources/halloweenify.json
+++ b/bot/resources/halloween/halloweenify.json
diff --git a/bot/resources/halloween/monstersurvey.json b/bot/resources/halloween/monstersurvey.json
new file mode 100644
index 00000000..99a3e96f
--- /dev/null
+++ b/bot/resources/halloween/monstersurvey.json
@@ -0,0 +1,30 @@
+{
+ "frankenstein": {
+ "full_name": "Frankenstein's Monster",
+ "summary": "His limbs were in proportion, and I had selected his features as beautiful. Beautiful! Great God! His yellow skin scarcely covered the work of muscles and arteries beneath; his hair was of a lustrous black, and flowing; his teeth of a pearly whiteness; but these luxuriances only formed a more horrid contrast with his watery eyes, that seemed almost of the same colour as the dun-white sockets in which they were set, his shrivelled complexion and straight black lips.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/a/a7/Frankenstein%27s_monster_%28Boris_Karloff%29.jpg",
+ "votes": []
+ },
+ "dracula": {
+ "full_name": "Count Dracula",
+ "summary": "Count Dracula is an undead, centuries-old vampire, and a Transylvanian nobleman who claims to be a Sz\u00c3\u00a9kely descended from Attila the Hun. He inhabits a decaying castle in the Carpathian Mountains near the Borgo Pass. Unlike the vampires of Eastern European folklore, which are portrayed as repulsive, corpse-like creatures, Dracula wears a veneer of aristocratic charm. In his conversations with Jonathan Harker, he reveals himself as deeply proud of his boyar heritage and nostalgic for the past, which he admits have become only a memory of heroism, honour and valour in modern times.",
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Bela_Lugosi_as_Dracula%2C_anonymous_photograph_from_1931%2C_Universal_Studios.jpg/250px-Bela_Lugosi_as_Dracula%2C_anonymous_photograph_from_1931%2C_Universal_Studios.jpg",
+ "votes": [
+ 224734305581137921
+ ]
+ },
+ "goofy": {
+ "full_name": "Goofy in the Monster's INC World",
+ "summary": "Pure nightmare fuel.\nThis monster is nothing like its original counterpart. With two different eyes, a pointed nose, fins growing out of its blue skin, and dark spots covering his body, he's a true nightmare come to life.",
+ "image": "https://www.dailydot.com/wp-content/uploads/3a2/a8/bf38aedbef9f795f.png",
+ "votes": []
+ },
+ "refisio": {
+ "full_name": "Refisio",
+ "summary": "Who let this guy write this? That's who the real monster is.",
+ "image": "https://avatars0.githubusercontent.com/u/24819750?s=460&v=4",
+ "votes": [
+ 95872159741644800
+ ]
+ }
+} \ No newline at end of file
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/utils/__init__.py
diff --git a/bot/utils/spookifications.py b/bot/utils/spookifications.py
new file mode 100644
index 00000000..5f2369ae
--- /dev/null
+++ b/bot/utils/spookifications.py
@@ -0,0 +1,55 @@
+import logging
+from random import choice, randint
+
+from PIL import Image
+from PIL import ImageOps
+
+log = logging.getLogger()
+
+
+def inversion(im):
+ """Inverts an image.
+
+ Returns an inverted image when supplied with an Image object.
+ """
+ im = im.convert('RGB')
+ inv = ImageOps.invert(im)
+ return inv
+
+
+def pentagram(im):
+ """Adds pentagram to image."""
+ im = im.convert('RGB')
+ wt, ht = im.size
+ penta = Image.open('bot/resources/halloween/bloody-pentagram.png')
+ penta = penta.resize((wt, ht))
+ im.paste(penta, (0, 0), penta)
+ return im
+
+
+def bat(im):
+ """Adds a bat silhoutte to the image.
+
+ The bat silhoutte is of a size at least one-fifths that of the original
+ image and may be rotated upto 90 degrees anti-clockwise."""
+ im = im.convert('RGB')
+ wt, ht = im.size
+ bat = Image.open('bot/resources/halloween/bat-clipart.png')
+ bat_size = randint(wt//10, wt//7)
+ rot = randint(0, 90)
+ bat = bat.resize((bat_size, bat_size))
+ bat = bat.rotate(rot)
+ x = randint(wt-(bat_size * 3), wt-bat_size)
+ y = randint(10, bat_size)
+ im.paste(bat, (x, y), bat)
+ im.paste(bat, (x + bat_size, y + (bat_size // 4)), bat)
+ im.paste(bat, (x - bat_size, y - (bat_size // 2)), bat)
+ return im
+
+
+def get_random_effect(im):
+ """Randomly selects and applies an effect."""
+ effects = [inversion, pentagram, bat]
+ effect = choice(effects)
+ log.info("Spookyavatar's chosen effect: " + effect.__name__)
+ return effect(im)
diff --git a/docker/build.sh b/docker/build.sh
deleted file mode 100755
index 153fbccc..00000000
--- a/docker/build.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-# Build and deploy on master branch
-if [[ $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then
- echo "Connecting to docker hub"
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
-
- echo "Building image"
- docker build -t pythondiscord/hacktober-bot:latest -f docker/Dockerfile .
-
- echo "Pushing image"
- docker push pythondiscord/hacktober-bot:latest
-
- echo "Deploying on server"
- pepper ${SALTAPI_TARGET} state.apply docker/hacktoberbot --out=no_out --non-interactive &> /dev/null
-else
- echo "Skipping deploy"
-fi
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 0a802ddc..de1f4cf2 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,11 +1,11 @@
version: "3"
services:
dumbo:
- image: pythondiscord/hacktober-bot:latest
- container_name: hacktoberbot
+ image: pythondiscord/seasonalbot:latest
+ container_name: seasonalbot
restart: always
environment:
- - HACKTOBERBOT_TOKEN
+ - SEASONALBOT_TOKEN
diff --git a/scripts/deploy-azure.sh b/scripts/deploy-azure.sh
new file mode 100755
index 00000000..abefbf6b
--- /dev/null
+++ b/scripts/deploy-azure.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+cd ..
+
+export SALTAPI_USER=$2
+export SALTAPI_PASS=$3
+export SALTAPI_URL=$4
+export SALTAPI_EAUTH=pam
+
+# Build and deploy on master branch, only if not a pull request
+if [[ ($BUILD_SOURCEBRANCHNAME == 'master') && ($SYSTEM_PULLREQUEST_PULLREQUESTID == '') ]]; then
+ echo "Building image"
+ docker build -t pythondiscord/seasonalbot:latest -f docker/Dockerfile .
+
+ echo "Pushing image to Docker Hub"
+ docker push pythondiscord/seasonalbot:latest
+
+ echo "Deploying on server"
+ pepper $1 state.apply docker/seasonalbot --out=no_out --non-interactive &> /dev/null
+else
+ echo "Skipping deploy"
+fi