This repository has been archived by the owner on Jul 4, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5309463
Showing
12 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
FROM python:latest | ||
|
||
WORKDIR /app | ||
ADD . /app | ||
|
||
RUN pip install -r requirements.txt | ||
|
||
CMD ["python", "run.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2017 Blob Emoji | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
============= | ||
BlobGivingBot | ||
============= | ||
|
||
.. image:: https://img.shields.io/badge/python-3.6-blue.svg | ||
:target: https://www.python.org/ | ||
|
||
.. image:: https://img.shields.io/badge/License-MIT-blue.svg | ||
:target: https://github.com/BlobEmoji/blobgivingbot/blob/master/LICENSE | ||
|
||
BlobGivingBot is a simple Discord Bot to hold giveaways in a single Discord Server. | ||
|
||
------------------- | ||
Using BlobGivingBot | ||
------------------- | ||
|
||
BlobGivingBot is meant for use in the `Blob Emoji Discord Server <https://discord.gg/xTf9URq>`_, | ||
there's no public bot instance you can simply invite. | ||
|
||
It is however really simple to run your own instance for your own Discord Servers: | ||
|
||
- Simply rename ``config_example.py`` to ``config.py`` and fill it out with your credentials. | ||
|
||
- Invoke commands in any specified command channels using your prefix. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from .bot import BlobGivingBot |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import discord | ||
from discord.ext import commands | ||
|
||
import config | ||
|
||
from .utils import Timer | ||
|
||
|
||
class BlobGivingBot(commands.AutoShardedBot): | ||
def __init__(self): | ||
super().__init__( | ||
command_prefix=config.prefix, | ||
owner_id=config.owner_id, | ||
) | ||
|
||
self.add_command(self.rtt) | ||
self.load_extension('blobgivingbot.giveaways') | ||
|
||
async def on_message(self, message: discord.Message): | ||
if message.author.bot: | ||
return | ||
|
||
if message.channel.id not in config.command_channels: | ||
return | ||
|
||
await self.process_commands(message) | ||
|
||
@commands.command(aliases=['ping', 'p']) | ||
async def rtt(self, ctx: commands.Context): | ||
"""Shows the bots HTTP and websocket latency to Discord.""" | ||
with Timer() as rtt: | ||
msg = await ctx.send('...') | ||
|
||
ws = self.latency * 1000 | ||
await msg.edit(content=f'Pong! rtt: {rtt}, ws: {ws:.3f}ms') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# copied from https://github.com/Rapptz/RoboDanny | ||
|
||
import json | ||
import os | ||
import uuid | ||
import asyncio | ||
|
||
|
||
def _create_encoder(cls): | ||
def _default(self, o): | ||
if isinstance(o, cls): | ||
return o.to_json() | ||
return super().default(o) | ||
|
||
return type('_Encoder', (json.JSONEncoder,), { 'default': _default }) | ||
|
||
|
||
class Config: | ||
"""The "database" object. Internally based on ``json``.""" | ||
|
||
def __init__(self, name, **options): | ||
self.name = f'{name}.json' | ||
self.object_hook = options.pop('object_hook', None) | ||
self.encoder = options.pop('encoder', None) | ||
|
||
self._db = None | ||
|
||
try: | ||
hook = options.pop('hook') | ||
except KeyError: | ||
pass | ||
else: | ||
self.object_hook = hook.from_json | ||
self.encoder = _create_encoder(hook) | ||
|
||
self.loop = options.pop('loop', asyncio.get_event_loop()) | ||
self.lock = asyncio.Lock() | ||
if options.pop('load_later', False): | ||
self.loop.create_task(self.load()) | ||
else: | ||
self.load_from_file() | ||
|
||
def load_from_file(self): | ||
try: | ||
with open(self.name, 'r') as f: | ||
self._db = json.load(f, object_hook=self.object_hook) | ||
except FileNotFoundError: | ||
self._db = {} | ||
|
||
async def load(self): | ||
with await self.lock: | ||
await self.loop.run_in_executor(None, self.load_from_file) | ||
|
||
def _dump(self): | ||
temp = '%s-%s.tmp' % (uuid.uuid4(), self.name) | ||
with open(temp, 'w', encoding='utf-8') as tmp: | ||
json.dump(self._db.copy(), tmp, ensure_ascii=True, cls=self.encoder, separators=(',', ':')) | ||
|
||
# atomically move the file | ||
os.replace(temp, self.name) | ||
|
||
async def save(self): | ||
with await self.lock: | ||
await self.loop.run_in_executor(None, self._dump) | ||
|
||
def get(self, key, *args): | ||
"""Retrieves a config entry.""" | ||
return self._db.get(str(key), *args) | ||
|
||
async def put(self, key, value, *args): | ||
"""Edits a config entry.""" | ||
self._db[str(key)] = value | ||
await self.save() | ||
|
||
async def remove(self, key): | ||
"""Removes a config entry.""" | ||
del self._db[str(key)] | ||
await self.save() | ||
|
||
def __contains__(self, item): | ||
return str(item) in self._db | ||
|
||
def __getitem__(self, item): | ||
return self._db[str(item)] | ||
|
||
def __len__(self): | ||
return len(self._db) | ||
|
||
def all(self): | ||
return self._db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import asyncio | ||
import datetime | ||
import logging | ||
import random | ||
|
||
import discord | ||
from discord.ext import commands | ||
|
||
from .bot import BlobGivingBot | ||
from .config import Config | ||
import config | ||
|
||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class NoWinnerFound(Exception): | ||
pass | ||
|
||
|
||
class Giveaways: | ||
"""Simple giveaway management commands.""" | ||
|
||
def __init__(self, bot: BlobGivingBot): | ||
self.bot = bot | ||
|
||
self.config = Config('giveaways') | ||
self._giveaway_task = bot.loop.create_task(self.giveaway_loop()) | ||
|
||
def __unload(self): | ||
self._giveaway_task.cancel() | ||
|
||
@property | ||
def emoji(self): | ||
return self.bot.get_emoji(config.giveaway_emoji) | ||
|
||
@property | ||
def channel(self): | ||
return self.bot.get_channel(config.giveaway_channel) | ||
|
||
@commands.command() | ||
async def giveaway(self, ctx: commands.Context, *, description: str): | ||
"""Start a new Giveaway.""" | ||
ends_at = ctx.message.created_at + config.giveaway_duration | ||
|
||
embed = discord.Embed( | ||
title=description, | ||
description=f'React with {self.emoji} to win!', | ||
color=discord.Color.magenta(), | ||
timestamp=ends_at, | ||
) | ||
embed.set_footer(text='Ends at') | ||
|
||
msg = await self.channel.send(embed=embed) | ||
await msg.add_reaction(self.emoji) | ||
|
||
await self.config.put(ends_at.timestamp(), msg.id) | ||
await ctx.send('Giveaway started!') | ||
|
||
if self._giveaway_task.done(): | ||
self._giveaway_task = self.bot.loop.create_task(self.giveaway_loop()) | ||
|
||
@commands.command() | ||
async def reroll(self, ctx: commands.Context, *, message_id: int): | ||
"""Reroll a giveaway based on a message/giveaway ID.""" | ||
try: | ||
message = await self.channel.get_message(message_id) | ||
except discord.NotFound: | ||
return await ctx.send(f'Couldn\'t find message with ID {message_id} in the giveaway channel!') | ||
|
||
# don't allow rerolling a giveaway which is running | ||
if message.created_at > ctx.message.created_at - config.giveaway_duration: | ||
return await ctx.send('This giveaway is still running! Rerolling can only be done after it has ended.') | ||
|
||
try: | ||
winner = await self.roll_user(message) | ||
except NoWinnerFound as e: | ||
await ctx.send(e) | ||
else: | ||
embed = message.embeds[0] | ||
giveaway_desc = embed.title | ||
|
||
embed.description = f'{winner.mention} is the new winner!' | ||
await message.edit(embed=embed) | ||
|
||
await ctx.send(f'Rerolled! {winner.mention} is the new winner for **{giveaway_desc}**!') | ||
|
||
async def giveaway_loop(self): | ||
# channels / emoji aren't loaded before being ready | ||
await self.bot.wait_until_ready() | ||
|
||
# run until Config.__len__ returns 0 | ||
while self.config: | ||
# the keys of the config are unix timestamps so we can easily sort them | ||
oldest = min(self.config.all()) | ||
|
||
until_end = float(oldest) - datetime.datetime.utcnow().timestamp() | ||
await asyncio.sleep(until_end) | ||
|
||
message_id = self.config.get(oldest) | ||
|
||
try: | ||
message = await self.channel.get_message(message_id) | ||
except discord.NotFound: | ||
# giveaways might be removed for moderation purposes, we don't want to post anything in this case | ||
log.warning(f'Couldn\'t find message associated with giveaway {message_id}, skipping') | ||
await self.config.remove(oldest) | ||
continue | ||
|
||
# prepare editing the embed | ||
embed = message.embeds[0] | ||
embed.set_footer(text='Ended at') | ||
|
||
# the name / title of this giveaway | ||
giveaway_desc = embed.title | ||
|
||
try: | ||
winner = await self.roll_user(message) | ||
except NoWinnerFound: | ||
embed.description = f'No one won this giveaway!' | ||
await message.edit(embed=embed) | ||
|
||
await self.channel.send(f'No winner found for **{giveaway_desc}**!') | ||
else: | ||
embed.description = f'{winner.mention} won this giveaway!' | ||
await message.edit(embed=embed) | ||
|
||
await self.channel.send(f'Congratulations {winner.mention}! You won **{giveaway_desc}**!') | ||
finally: | ||
await self.config.remove(oldest) | ||
|
||
async def roll_user(self, message: discord.Message) -> discord.Member: | ||
try: | ||
reaction = next(x for x in message.reactions if getattr(x.emoji, 'id', None) == self.emoji.id) | ||
except StopIteration: # if a moderator deleted the emoji for some reason | ||
raise NoWinnerFound('Couldn\'t find giveaway emoji on specified message') | ||
|
||
users = await reaction.users().filter(lambda x: not x.bot).flatten() | ||
if not users: | ||
raise NoWinnerFound('No human reacted with the giveaway emoji on this message') | ||
else: | ||
return random.choice(users) | ||
|
||
|
||
def setup(bot: BlobGivingBot): | ||
bot.add_cog(Giveaways(bot)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import time | ||
|
||
|
||
class Timer: | ||
"""Context manager to measure how long the indented block takes to run.""" | ||
|
||
def __init__(self): | ||
self.start: float = None | ||
self.end: float = None | ||
|
||
def __enter__(self): | ||
self.start = time.perf_counter() | ||
return self | ||
|
||
async def __aenter__(self): | ||
return self.__enter__() | ||
|
||
def __exit__(self, exc_type, exc_val, exc_tb): | ||
self.end = time.perf_counter() | ||
|
||
async def __aexit__(self, exc_type, exc_val, exc_tb): | ||
return self.__exit__(exc_type, exc_val, exc_tb) | ||
|
||
def __str__(self): | ||
return f'{self.duration:.3f}ms' | ||
|
||
@property | ||
def duration(self): | ||
"""Duration in ms.""" | ||
return (self.end - self.start) * 1000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
version: '2' | ||
|
||
services: | ||
|
||
bot: | ||
build: . | ||
volumes: | ||
- ./:/app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import datetime | ||
|
||
token = 'beep.boop.secret' | ||
|
||
prefix = '$' | ||
owner_id = 69198249432449024 | ||
|
||
giveaway_channel = 384121527437885440 | ||
giveaway_emoji = 384394753749417996 | ||
|
||
giveaway_duration = datetime.timedelta(days=1) | ||
|
||
# channels in which the bot responds to commands, all others are ignored | ||
command_channels = {384121527437885440} |
Oops, something went wrong.