diff options
author | 2018-10-14 17:56:21 +0530 | |
---|---|---|
committer | 2018-10-14 17:56:21 +0530 | |
commit | 394b3a29ce6106bbc841d3377724180d85001788 (patch) | |
tree | 77c94655dc9210410989934c213b2bdcf46c82cc | |
parent | Merge pull request #34 from markylon/master (diff) | |
parent | Issue #15 Feature pull request template (#49) (diff) |
Merge pull request #1 from discord-python/master
Merge
-rw-r--r-- | .github/PULL_REQUEST_TEMPLATE/pull_request_template.md | 21 | ||||
-rw-r--r-- | .travis.yml | 17 | ||||
-rw-r--r-- | Pipfile | 3 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | bot/__init__.py | 25 | ||||
-rw-r--r-- | bot/__main__.py (renamed from bot/bot.py) | 24 | ||||
-rw-r--r-- | bot/cogs/hacktoberstats.py | 9 | ||||
-rw-r--r-- | bot/cogs/halloween_facts.py | 75 | ||||
-rw-r--r-- | bot/cogs/halloweenify.py | 9 | ||||
-rw-r--r-- | bot/cogs/movie.py | 89 | ||||
-rw-r--r-- | bot/cogs/spookyreact.py | 31 | ||||
-rw-r--r-- | bot/cogs/template.py | 2 | ||||
-rw-r--r-- | bot/resources/halloween_facts.json | 14 | ||||
-rw-r--r-- | bot/resources/halloweenify.json | 3 | ||||
-rw-r--r-- | docker/Dockerfile | 15 | ||||
-rwxr-xr-x | docker/build.sh | 18 | ||||
-rw-r--r-- | docker/docker-compose.yml | 11 | ||||
-rw-r--r-- | tox.ini | 2 |
18 files changed, 317 insertions, 55 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..c8b15b09 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,21 @@ +--- +name: Pull Request +about: A simple pull request template. +issue: Issue # if applicable + +--- + +Provide a simple description of what the PR achieves. + +## Pull Request Details + +Please ensure your PR fulfills the following criteria: + +- [ ] Have you joined the [PythonDiscord Community](https://pythondiscord.com/invite)? +- [ ] Were your changes made in a Pipenv environment? +- [ ] Does flake8 pass (```pipenv run lint```) + + +## Additional information + +Provide any additional information or clarifications here.
\ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ae1d7653 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +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 @@ -18,4 +18,5 @@ name = "pypi" python_version = "3.7" [scripts] -start = "python -m bot"
\ No newline at end of file +start = "python -m bot" +lint = "flake8 bot" @@ -14,6 +14,10 @@ We know it can be difficult to get into the whole open source thing at first. To !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 + ## Getting started If you are new to this you will find it far easier using PyCharm: diff --git a/bot/__init__.py b/bot/__init__.py index 8cbcd121..c411deb6 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -1,10 +1,11 @@ -import os import logging.handlers +import os +from pathlib import Path -# set up logging -log_dir = 'log' -log_file = log_dir + os.sep + 'hackbot.log' +# set up logging +log_dir = Path("bot", "log") +log_file = log_dir / "hackbot.log" os.makedirs(log_dir, exist_ok=True) # file handler sets up rotating logs every 5 MB @@ -22,9 +23,15 @@ if root.handlers: for handler in root.handlers: root.removeHandler(handler) -# setup new logging configuration -logging.basicConfig(format='%(asctime)s - %(name)s %(levelname)s: %(message)s', datefmt="%D %H:%M:%S", - level=logging.DEBUG, - handlers=[console_handler, file_handler]) +# Silence irrelevant loggers +logging.getLogger("discord").setLevel(logging.ERROR) +logging.getLogger("websockets").setLevel(logging.ERROR) -logging.info('Logging Process Started')
\ No newline at end of file +# setup new logging configuration +logging.basicConfig( + format='%(asctime)s - %(name)s %(levelname)s: %(message)s', + datefmt="%D %H:%M:%S", + level=logging.DEBUG, + handlers=[console_handler, file_handler] +) +logging.getLogger().info('Logging initialization complete') diff --git a/bot/bot.py b/bot/__main__.py index a40ed0d4..ccd69b0b 100644 --- a/bot/bot.py +++ b/bot/__main__.py @@ -1,39 +1,39 @@ +import logging from os import environ from pathlib import Path -from sys import stderr -from traceback import print_exc, format_exc +from traceback import format_exc from discord.ext import commands -import logging HACKTOBERBOT_TOKEN = environ.get('HACKTOBERBOT_TOKEN') +log = logging.getLogger() if HACKTOBERBOT_TOKEN: token_dl = len(HACKTOBERBOT_TOKEN) // 8 - logging.info(f'Bot token loaded: {HACKTOBERBOT_TOKEN[:token_dl]}...{HACKTOBERBOT_TOKEN[-token_dl:]}') + log.info(f'Bot token loaded: {HACKTOBERBOT_TOKEN[:token_dl]}...{HACKTOBERBOT_TOKEN[-token_dl:]}') else: - logging.error(f'Bot token not found: {HACKTOBERBOT_TOKEN}') + log.error(f'Bot token not found: {HACKTOBERBOT_TOKEN}') ghost_unicode = "\N{GHOST}" bot = commands.Bot(command_prefix=commands.when_mentioned_or(".", f"{ghost_unicode} ", ghost_unicode)) -logging.info('Start loading extensions from ./cogs/') +log.info('Start loading extensions from ./bot/cogs/') 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('cogs').glob('*.py')] + cogs = [file.stem for file in Path('bot', 'cogs').glob('*.py')] for extension in cogs: try: - bot.load_extension(f'cogs.{extension}') - logging.info(f'Successfully loaded extension: {extension}') + bot.load_extension(f'bot.cogs.{extension}') + log.info(f'Successfully loaded extension: {extension}') except Exception as e: - logging.error(f'Failed to load extension {extension}: {repr(e)} {format_exc()}') + log.error(f'Failed to load extension {extension}: {repr(e)} {format_exc()}') # print(f'Failed to load extension {extension}.', file=stderr) # print_exc() -logging.info(f'Spooky Launch Sequence Initiated...') +log.info(f'Spooky Launch Sequence Initiated...') bot.run(HACKTOBERBOT_TOKEN) -logging.info(f'HackBot has been slain!')
\ No newline at end of file +log.info(f'HackBot has been slain!') diff --git a/bot/cogs/hacktoberstats.py b/bot/cogs/hacktoberstats.py index 4e896ae9..ac81b887 100644 --- a/bot/cogs/hacktoberstats.py +++ b/bot/cogs/hacktoberstats.py @@ -95,7 +95,14 @@ class Stats: is_query = f"public+author:{username}" date_range = "2018-10-01..2018-10-31" per_page = "300" - query_url = f"{base_url}-label:{not_label}+type:{action_type}+is:{is_query}+created:{date_range}&per_page={per_page}" + 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: diff --git a/bot/cogs/halloween_facts.py b/bot/cogs/halloween_facts.py new file mode 100644 index 00000000..e97c80d2 --- /dev/null +++ b/bot/cogs/halloween_facts.py @@ -0,0 +1,75 @@ +import asyncio +import json +import random +from datetime import timedelta +from pathlib import Path + +import discord +from discord.ext import commands + +SPOOKY_EMOJIS = [ + "\N{BAT}", + "\N{DERELICT HOUSE BUILDING}", + "\N{EXTRATERRESTRIAL ALIEN}", + "\N{GHOST}", + "\N{JACK-O-LANTERN}", + "\N{SKULL}", + "\N{SKULL AND CROSSBONES}", + "\N{SPIDER WEB}", +] +PUMPKIN_ORANGE = discord.Color(0xFF7518) +HACKTOBERBOT_CHANNEL_ID = 498804484324196362 +INTERVAL = timedelta(hours=6).total_seconds() + + +class HalloweenFacts: + + def __init__(self, bot): + self.bot = bot + with open(Path("./bot/resources", "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.bot.loop.create_task(self._fact_publisher_task()) + + async def _fact_publisher_task(self): + """ + A background task that runs forever, sending Halloween facts at random to the Discord channel with id equal to + HACKTOBERFEST_CHANNEL_ID every INTERVAL seconds. + """ + facts = list(enumerate(self.halloween_facts)) + while True: + # Avoid choosing each fact at random to reduce chances of facts being reposted soon. + random.shuffle(facts) + for index, fact in facts: + embed = self._build_embed(index, fact) + await self.channel.send("Your regular serving of random Halloween facts", embed=embed) + self.last_fact = (index, fact) + await asyncio.sleep(INTERVAL) + + @commands.command(name="hallofact", aliases=["hallofacts"], brief="Get the most recent Halloween fact") + async def get_last_fact(self, ctx): + """ + Reply with the most recent Halloween fact. + """ + if ctx.channel != self.channel: + return + index, fact = self.last_fact + embed = self._build_embed(index, fact) + await ctx.send("Halloween fact recap", embed=embed) + + @staticmethod + def _build_embed(index, fact): + """ + Builds a Discord embed from the given fact and its index. + """ + emoji = random.choice(SPOOKY_EMOJIS) + title = f"{emoji} Halloween Fact #{index + 1}" + return discord.Embed(title=title, description=fact, color=PUMPKIN_ORANGE) + + +def setup(bot): + bot.add_cog(HalloweenFacts(bot)) diff --git a/bot/cogs/halloweenify.py b/bot/cogs/halloweenify.py index 8a9db3df..a5fe45ef 100644 --- a/bot/cogs/halloweenify.py +++ b/bot/cogs/halloweenify.py @@ -1,15 +1,13 @@ -from pathlib import Path from json import load +from pathlib import Path from random import choice - import discord from discord.ext import commands from discord.ext.commands.cooldowns import BucketType class Halloweenify: - """ A cog to change a invokers nickname to a spooky one! """ @@ -20,7 +18,10 @@ class Halloweenify: @commands.cooldown(1, 300, BucketType.user) @commands.command() async def halloweenify(self, ctx): - with open(Path('../bot/resources', 'halloweenify.json'), 'r') as f: + """ + Change your nickname into a much spookier one! + """ + with open(Path('./bot/resources', '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/movie.py b/bot/cogs/movie.py index bb6f8df8..925f813f 100644 --- a/bot/cogs/movie.py +++ b/bot/cogs/movie.py @@ -1,8 +1,10 @@ -import requests import random from os import environ -from discord.ext import commands + +import aiohttp from discord import Embed +from discord.ext import commands + TMDB_API_KEY = environ.get('TMDB_API_KEY') TMDB_TOKEN = environ.get('TMDB_TOKEN') @@ -16,8 +18,11 @@ class Movie: def __init__(self, bot): self.bot = bot - @commands.command(name='movie', alias=['tmdb'], brief='Pick a scary movie') + @commands.command(name='movie', alias=['tmdb']) async def random_movie(self, ctx): + """ + Randomly select a scary movie and display information about it. + """ selection = await self.select_movie() movie_details = await self.format_metadata(selection) @@ -40,19 +45,24 @@ class Movie: } # Get total page count of horror movies - response = requests.get(url=url, params=params, headers=headers) - total_pages = response.json().get('total_pages') + async with aiohttp.ClientSession() as session: + response = await session.get(url=url, params=params, headers=headers) + total_pages = await response.json() + total_pages = total_pages.get('total_pages') - # Get movie details from one random result on a random page - params['page'] = random.randint(1, total_pages) - response = requests.get(url=url, params=params, headers=headers) - selection_id = random.choice(response.json().get('results')).get('id') + # Get movie details from one random result on a random page + params['page'] = random.randint(1, total_pages) + response = await session.get(url=url, params=params, headers=headers) + response = await response.json() + selection_id = random.choice(response.get('results')).get('id') - # Get full details and credits - selection = requests.get(url='https://api.themoviedb.org/3/movie/' + str(selection_id), - params={'api_key': TMDB_API_KEY, 'append_to_response': 'credits'}) + # Get full details and credits + selection = await session.get( + url='https://api.themoviedb.org/3/movie/' + str(selection_id), + params={'api_key': TMDB_API_KEY, 'append_to_response': 'credits'} + ) - return selection.json() + return await selection.json() @staticmethod async def format_metadata(movie): @@ -60,36 +70,63 @@ class Movie: Formats raw TMDb data to be embedded in discord chat """ - tmdb_url = 'https://www.themoviedb.org/movie/' + str(movie.get('id')) - poster = 'https://image.tmdb.org/t/p/original' + movie.get('poster_path') + # Build the relevant URLs. + movie_id = movie.get("id") + poster_path = movie.get("poster_path") + tmdb_url = f'https://www.themoviedb.org/movie/{movie_id}' if movie_id else None + poster = f'https://image.tmdb.org/t/p/original{poster_path}' if poster_path else None + # Get cast names cast = [] - for actor in movie.get('credits').get('cast')[:3]: + for actor in movie.get('credits', {}).get('cast', [])[:3]: cast.append(actor.get('name')) - director = movie.get('credits').get('crew')[0].get('name') + # Get director name + director = movie.get('credits', {}).get('crew', []) + if director: + director = director[0].get('name') - rating_count = movie.get('vote_average') / 2 + # Determine the spookiness rating rating = '' + rating_count = movie.get('vote_average', 0) - for i in range(int(rating_count)): - rating += ':skull:' + if rating_count: + rating_count /= 2 + for _ in range(int(rating_count)): + rating += ':skull:' if (rating_count % 1) >= .5: rating += ':bat:' + # Try to get year of release and runtime + year = movie.get('release_date', [])[:4] + runtime = movie.get('runtime') + runtime = f"{runtime} minutes" if runtime else None + + # Not all these attributes will always be present + movie_attributes = { + "Directed by": director, + "Starring": ', '.join(cast), + "Running time": runtime, + "Release year": year, + "Spookiness rating": rating, + } + embed = Embed( colour=0x01d277, title='**' + movie.get('title') + '**', url=tmdb_url, description=movie.get('overview') ) - embed.set_image(url=poster) - embed.add_field(name='Starring', value=', '.join(cast)) - embed.add_field(name='Directed by', value=director) - embed.add_field(name='Year', value=movie.get('release_date')[:4]) - embed.add_field(name='Runtime', value=str(movie.get('runtime')) + ' min') - embed.add_field(name='Spooky Rating', value=rating) + + if poster: + embed.set_image(url=poster) + + # Add the attributes that we actually have data for, but not the others. + for name, value in movie_attributes.items(): + if value: + embed.add_field(name=name, value=value) + embed.set_footer(text='powered by themoviedb.org') return embed diff --git a/bot/cogs/spookyreact.py b/bot/cogs/spookyreact.py new file mode 100644 index 00000000..2652a60e --- /dev/null +++ b/bot/cogs/spookyreact.py @@ -0,0 +1,31 @@ +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 b3f4da21..aa01432c 100644 --- a/bot/cogs/template.py +++ b/bot/cogs/template.py @@ -17,7 +17,7 @@ class Template: """ await ctx.send('https://github.com/discord-python/hacktoberbot') - @commands.group(name='git', invoke_without_command=True) + @commands.group(name='git', invoke_without_command=True, brief="A link to resources for learning Git") async def github(self, ctx): """ A command group with the name git. You can now create sub-commands such as git commit. diff --git a/bot/resources/halloween_facts.json b/bot/resources/halloween_facts.json new file mode 100644 index 00000000..fc6fa85f --- /dev/null +++ b/bot/resources/halloween_facts.json @@ -0,0 +1,14 @@ +[ + "Halloween or Hallowe'en is also known as Allhalloween, All Hallows' Eve and All Saints' Eve.", + "It is widely believed that many Halloween traditions originated from ancient Celtic harvest festivals, particularly the Gaelic festival Samhain, which means \"summer's end\".", + "It is believed that the custom of making jack-o'-lanterns at Halloween began in Ireland. In the 19th century, turnips or mangel wurzels, hollowed out to act as lanterns and often carved with grotesque faces, were used at Halloween in parts of Ireland and the Scottish Highlands.", + "Halloween is the second highest grossing commercial holiday after Christmas.", + "The word \"witch\" comes from the Old English *wicce*, meaning \"wise woman\". In fact, *wiccan* were highly respected people at one time. According to popular belief, witches held one of their two main meetings, or *sabbats*, on Halloween night.", + "Samhainophobia is the fear of Halloween.", + "The owl is a popular Halloween image. In Medieval Europe, owls were thought to be witches, and to hear an owl's call meant someone was about to die.", + "An Irish legend about jack-o'-lanterns goes as follows:\n*On route home after a night's drinking, Jack encounters the Devil and tricks him into climbing a tree. A quick-thinking Jack etches the sign of the cross into the bark, thus trapping the Devil. Jack strikes a bargain that Satan can never claim his soul. After a life of sin, drink, and mendacity, Jack is refused entry to heaven when he dies. Keeping his promise, the Devil refuses to let Jack into hell and throws a live coal straight from the fires of hell at him. It was a cold night, so Jack places the coal in a hollowed out turnip to stop it from going out, since which time Jack and his lantern have been roaming looking for a place to rest.*", + "Trick-or-treating evolved from the ancient Celtic tradition of putting out treats and food to placate spirits who roamed the streets at Samhain, a sacred festival that marked the end of the Celtic calendar year.", + "Comedian John Evans once quipped: \"What do you get if you divide the circumference of a jack-o’-lantern by its diameter? Pumpkin π.\"", + "Dressing up as ghouls and other spooks originated from the ancient Celtic tradition of townspeople disguising themselves as demons and spirits. The Celts believed that disguising themselves this way would allow them to escape the notice of the real spirits wandering the streets during Samhain.", + "In Western history, black cats have typically been looked upon as a symbol of evil omens, specifically being suspected of being the familiars of witches, or actually shape-shifting witches themselves. They are, however, too cute to be evil." +] diff --git a/bot/resources/halloweenify.json b/bot/resources/halloweenify.json index 458f9342..88c46bfc 100644 --- a/bot/resources/halloweenify.json +++ b/bot/resources/halloweenify.json @@ -74,6 +74,9 @@ }, { "Chatterer": "https://c-5uwzmx78pmca09x24quoqfx2ezivsmzx2ekwu.g00.ranker.com/g00/3_c-5eee.zivsmz.kwu_/c-5UWZMXPMCA09x24pbbx78ax3ax2fx2fquoqf.zivsmz.kwux2fvwlm_quox2f14x2f586061x2fwzqoqvitx2fkpibbmzmz-nqtu-kpizikbmza-x78pwbw-9x3fex3d438x26yx3d48x26nux3drx78ox26nqbx3dkzwx78x26kzwx78x3dnikmax22x26q98k.uizsx3dquiom_$/$/$/$/$/$" + }, + { + "Pale Man": "https://i2.wp.com/macguff.in/wp-content/uploads/2016/10/Pans-Labyrinth-Movie-Header-Image.jpg?fit=630%2C400&ssl=1" } ] }
\ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..9c4406bf --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.6-alpine3.7 +RUN apk add --update tini git + +RUN mkdir /bot +COPY . /bot +WORKDIR /bot + +ENV LIBRARY_PATH=/lib:/usr/lib + +RUN pip install pipenv +RUN pipenv install --deploy --system + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["python", "-m", "bot"] + diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 00000000..153fbccc --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,18 @@ +#!/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 new file mode 100644 index 00000000..0a802ddc --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3" +services: + dumbo: + image: pythondiscord/hacktober-bot:latest + container_name: hacktoberbot + + restart: always + + environment: + - HACKTOBERBOT_TOKEN + @@ -1,6 +1,6 @@ [flake8] max-line-length=120 -application_import_names=proj +application_import_names=bot ignore=P102,B311,W503,E226,S311 exclude=__pycache__, venv, .venv, tests import-order-style=pycharm |