Initial commit

This commit is contained in:
bytedream 2022-04-20 23:41:25 +02:00
commit 0a43da51f1
16 changed files with 2185 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
__pycache__/

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM python:3.9
ADD . /kaizen-bot
RUN apt-get update -y && \
apt-get install mariadb-client mariadb-server python3-pip -y && \
./kaizen-bot-old/install.sh
CMD ["python", "/kaizen-bot/main.py", "run"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 ByteDream
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.

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# Kaizen bot (old)
Code for the discord bot of the official discord server from the german anime youtuber [kaizen](https://www.youtube.com/c/KaizenAnime).
It is outdated and ran until half a year ago, seen at the time of writing (20.04.2022).
The only changes I made to the code is removing hardcoded tokens (very secure I know) and add some comments just because I've felt the urge to comment the mess.
Additional comments are prefixed with `ADDED AFTERWARDS: `.
I don't know if the code is able to run anymore, may some dependencies broke, have changed or something else ¯\_(ツ)_/¯.
Feel free to use the code for whatever you want, but as a warning: it's ugly and nested af.

BIN
assets/kaizen-round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
assets/kaizen.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
assets/man_of_culture.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/rules.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

5
install.sh Normal file
View File

@ -0,0 +1,5 @@
apt install git libmariadb3 libmariadb-dev
pip3 install aiohttp discord.py dreamutils mariadb tweepy twitchAPI git+git://github.com/Rapptz/discord-ext-menus
touch /var/log/kaizen.log
chmod ugo+rwx /var/log/kaizen.log

26
kaizenbot/__init__.py Normal file
View File

@ -0,0 +1,26 @@
import logging
logger = logging.getLogger('kaizen')
# sets the logger output format
formatter = logging.Formatter('[%(asctime)s] - %(levelname)s: %(message)s', '%Y-%m-%d %H:%M:%S')
# creates the logger handlers
# configuring the logger
logger.setLevel(logging.DEBUG)
# console output
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# file output
file_handler = logging.FileHandler('/var/log/kaizen.log')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# --- #
# ADDED AFTERWARDS: yes a global variable, why use classes if this works too
glob = {'bot': None, 'transcript': {}, 'timers': [], 'twitter': None}

679
kaizenbot/commands.py Normal file
View File

@ -0,0 +1,679 @@
import asyncio
from copy import copy
import datetime
import discord
from discord.ext import commands, menus
from pathlib import Path
import os
import random
import re
import requests
import signal
import tempfile
import time
from typing import List, Optional
from . import logger, glob
from . import flags
from .utils import Embeds, MenuListPageSource, role_names
class Chat(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.flag_parser = flags.Flag(raise_errors=False)
self.flag_parser.add_flag('--all',
store_type=flags.StoreType.store_bool,
allowed_roles=['Mod'],
not_allowed_role_message='Nur Mods können die `{flag}` flag benutzen',
wrong_store_type_message='Die `{flag}` flag darf keinen parameter haben',
show_help=False)
self.flag_parser.add_flag('--limit',
store_type=(flags.StoreType.store_value, 100),
parser=self._parse_history,
wrong_store_type_message='Die {flag} muss eine Zahl als parameter haben',
help='')
async def _parse_history(self, ctx: commands.Context, flag, value):
try:
return int(value)
except ValueError:
await ctx.send(f'Der parameter der `{flag}` muss eine Zahl sein')
return False
@commands.command(name='export', ignore_extra=False,
usage='export [flags]', help='Exportiert den Chat',
flags='flag_parser')
async def history(self, ctx: commands.Context, *, args):
parsed = await self.flag_parser.parse(args, ctx=ctx)
pass
# ADDED AFTERWARS: This never was really active in use
class Economy(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.cooldown = {}
self.cooldown_date = datetime.datetime.now()
self.shop_items = {
'color': [10000, self.bot.database.set_user_color_count, 'Nachrichten Farbe', True, 'Setzt die Farbe die rechts bei den Bot Nachrichten, die von <@!802659482693926943> kommen'],
'extra vote': [150, self.bot.database.set_user_extra_vote, 'Extra Vote', False, 'Kann eine Umfrage starten, selbst wenn man normalerweise noch Umfrage cooldown hätte']
}
@commands.command(name='buy', aliases=['b'], usage='buy <zu kaufendes item>', help='Kauft ein Shop Item. Tippe `$shop` um alle verfügbaren Items zu sehen')
async def buy(self, ctx: commands.Context, *item):
await ctx.send(embed=Embeds.error_embed(description='Der Befehl steht wegen bearbeitung temporär nicht zur Verfügung'))
return
to_buy = ' '.join(item)
if values := self.shop_items.get(to_buy, None):
if self.bot.database.has_remove_user_gold(ctx.author.id, values[0]):
embed = discord.Embed(title='Item kauf')
embed.add_field(name=values[2], value=f'*{values[4]}*\n\n'
f'<@!{ctx.author.id}> erhält:\n'
f'```diff\n'
f'+1 {values[2]}'
f'```\n'
f'<@!{ctx.author.id}> bezahlt:\n'
f'```diff\n'
f'- {values[0]} Gold\n'
f'```')
message = await ctx.send(embed=embed)
await message.add_reaction('')
await message.add_reaction('')
def check(reaction: discord.Reaction, user):
return user.id == ctx.author.id and reaction.message.id == message.id and str(reaction.emoji) in ['', '']
try:
reaction, user = await self.bot.wait_for('reaction_add', timeout=30, check=check)
if reaction.emoji == '':
embed.colour = 0xff0000
msg = 'Der Kauf wurde abgebrochen'
elif reaction.emoji == '':
values[1](ctx.author.id, 1)
embed.colour = 0x00ff00
msg = f'**{values[2]}** wurde gekauft{f". Tippe `$use {to_buy}` um das Item zu benutzen!" if values[3] else "!"}'
else:
msg = 'Hackerman hat den Bot überlistet, weshalb diese Nachricht erscheint, die eigentlich niemals hätte erscheinen dürfen'
embed.clear_fields()
embed.add_field(name=values[2], value=f'*{values[4]}*\n\n'
f'<@!{ctx.author.id}> erhält:\n'
f'```diff\n'
f'+1 {values[2]}'
f'```\n'
f'<@!{ctx.author.id}> bezahlt:\n'
f'```diff\n'
f'- {values[0]} Gold\n'
f'```\n'
f'{msg}')
await message.edit(embed=embed)
except asyncio.exceptions.TimeoutError:
pass
else:
await ctx.send(embed=Embeds.error_embed(description='Du hast nicht genügend Gold um dieses Item zu kaufen'))
else:
await ctx.send(embed=Embeds.error_embed(description=f'Es existiert kein Item mit dem Name `{to_buy}`'))
@commands.command(name='items', aliases=['i'], usage='items', help='Zeigt alle Items im besitzt an')
async def items(self, ctx: commands.Context, *, user_mention: str = None):
await ctx.send(embed=Embeds.error_embed(description='Der Befehl steht wegen bearbeitung temporär nicht zur Verfügung'))
return
id = ctx.author.id
if user_mention is not None:
regex_id = re.match(r'^<@(!)?(?P<id>\d{18})>', user_mention)
if regex_id:
id = regex_id.group('id')
else:
await Help(self.bot).show_help(ctx, ctx.command)
return
embeds = []
embed = discord.Embed(title='Items', description=f'Items von <@!{id}>')
embed.set_footer(text='Alle Items die mit 💫 beginnen können über `$use <item>` benutzt werden!')
for name, value in self.bot.database.get_user_items(id).items():
if len(embed.fields) >= 10:
embeds.append(embed)
embed = discord.Embed(title='Items', description=f'Items von <@!{id}>')
embed.set_footer(text='Alle Items die mit 💫 beginnen können über `$use <item>` benutzt werden!')
if name == 'gold':
embed.description += f'\n\n:moneybag: **{value}** *Gold*'
elif value > 0:
item = self.shop_items[name]
embed.description += f'\n{":dizzy:" if item[3] else ":sparkles:"} **{value}** · `{name}` · *{item[2]}*'
embeds.append(embed)
send_embeds = menus.MenuPages(source=MenuListPageSource(embeds), clear_reactions_after=True, timeout=30)
await send_embeds.start(ctx)
@commands.command(name='leaderboard', aliases=['l'], usage='leaderboard', help='Zeigt das Server Leaderboard an')
async def leaderboard(self, ctx: commands.Context):
await ctx.send(embed=Embeds.error_embed(description='Der Befehl steht wegen bearbeitung temporär nicht zur Verfügung'))
return
gold_leaderboard = discord.Embed(title='Gold Leaderboard', description='*Man kann alle 2 Minuten zwischen 5 und 15 Gold bekommen, wenn man in diesen Zeitabständen eine Nachricht schreibt. '
'Die gedroppte Gold Anzahl wird mit deinem Kaizen-Sub Level addiert (Server Booster gelten als Sub Level 2).\n'
'Also je aktiver, desto mehr Gold:moneybag:^^*')
gold_leaderboard.set_footer(text='Tippe `$shop` um dir anzusehen, was du alles kaufen kannst!')
gold_text = ''
for i, (id, gold) in enumerate(self.bot.database.get_leaderboard().items(), 1):
gold_text += f'\n{i}. - `{gold}` Gold · __**{(await self.bot.guild.fetch_member(id)).display_name}**__'
gold_leaderboard.add_field(name='Top 10', value=gold_text)
menu = menus.MenuPages(source=MenuListPageSource([gold_leaderboard]))
await menu.start(ctx)
@commands.command(name='shop', aliases=['s'], usage='shop', help='Zeigt alle Shop Elemente an')
async def shop(self, ctx: commands.Context):
await ctx.send(embed=Embeds.error_embed(description='Der Befehl steht wegen bearbeitung temporär nicht zur Verfügung'))
return
embeds = []
embed = discord.Embed(title='Show durchsuchen')
for name, item in self.shop_items.items():
if len(embed.fields) >= 10:
embeds.append(embed)
embed = discord.Embed(title='Show durchsuchen')
embed.add_field(name=item[2], value=f'*{item[4]}*\n'
f'```diff\n'
f'- {item[0]} Gold\n'
f'+ 1 {item[2]}\n'
f'> $buy {name}\n'
f'```', inline=False)
embeds.append(embed)
embeds = menus.MenuPages(source=MenuListPageSource(embeds), clear_reactions_after=True, timeout=30)
await embeds.start(ctx)
@commands.command(name='use', aliases=['u'], usage='use', help='Benutzt ein Item')
async def use(self, ctx: commands.Context, *args):
await ctx.send(embed=Embeds.error_embed(description='Der Befehl steht wegen bearbeitung temporär nicht zur Verfügung'))
return
pass
# --- #
@commands.Cog.listener()
@commands.guild_only()
async def on_message(self, message: discord.Message):
return
if not message.author.bot and message.channel.id not in [812436978809307156, # rank abfrage
822938804461502524, # offene daten
813135345725341736, # karuta
813886662841073684, # rin
813487848027193398]: # quiz erraten
now = time.time()
cooldown = self.cooldown.get(message.author.id, [0.0, 0])
if cooldown[1] < 120 and cooldown[0] + 120 < now:
cooldown[0] = now
cooldown[1] += 1
self.cooldown[message.author.id] = cooldown
gold = random.randint(5, 15)
roles = role_names(message.author)
if 'Kaizen-Sub: Level 3' in roles:
gold += 3
elif 'Kaizen-Sub: Level 2' in roles:
gold += 2
elif 'Kaizen-Sub: Level 1' in roles:
gold += 1
if 'Server Booster' in roles:
gold += 2
self.bot.database.add_user_gold(message.author.id, gold)
def reset(self):
for key in self.cooldown:
self.cooldown[key] = [0.0, 0]
# ADDED AFTERWARDS: I had the idea of implementing familiarly relationship and show it in a tree
# but this was never finished (not that I really started to try working on it)
class Family(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command(name='tree', usage='tree', help='Zeigt den Familienstammbaum')
@commands.guild_only()
async def family_tree(self, ctx: commands.Context):
self.bot.database.get_user_parents(ctx.author.id)
def _family_tree(self, id: int) -> List[List[str]]:
parents = self.bot.database.get_user_parents()
if parents:
return [].extend()
else:
return [self.bot.guild.fetch_member(id).display_name]
class Mod(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command(name='error', usage='error')
@commands.guild_only()
@commands.has_role('Mod')
async def error(self, ctx: commands.Context):
logger.info('NOTICE: Raising controlled exception...')
raise Exception('Exception raised by the error command')
@commands.command(name='restart', usage='restart')
@commands.guild_only()
@commands.has_role('Mod')
async def restart(self, ctx: commands.Context):
for thread in glob['timers']:
thread.cancel()
logger.info('Restarting...')
os.kill(os.getpid(), signal.SIGKILL)
@commands.command(name='transcript', usage='transcript', help='Schickt eine Datei mit dem gesamten Nachrichtenverlauf der letzten Stunde')
@commands.guild_only()
@commands.has_role('Mod')
async def transcript(self, ctx: commands.Context):
tmp = tempfile.mktemp('.txt')
with open(tmp, 'w+') as file:
for message in glob['transcript'].values():
file.write(message + '\n')
file.close()
await ctx.send(file=discord.File(tmp, 'transcript.txt'))
os.remove(tmp)
class Help(commands.Cog):
def __init__(self, bot):
self.bot = bot
async def show_help(self, ctx: commands.Context, command: commands.Command):
embed = discord.Embed(title=f'`{command.name}` command\n\n', color=discord.Color(0xff0000))
embed.set_footer(text='<...> - required; [...] - optional; <...|...> - or\nDo NOT include <>, [] or | when executing the command')
embed.add_field(name='Usage', value=f'`{self.bot.command_prefix}{command.usage}`', inline=False)
embed.add_field(name='Description', value=command.help, inline=False)
if command_flags := flags.get_flags(command):
all_flags = {}
for flag, info in command_flags.flags().items():
if info['show_help']:
hash_sum = hash(str(info))
if hash_sum not in all_flags:
all_flags[hash_sum] = copy(info)
all_flags[hash_sum]['names'] = [flag]
else:
all_flags[hash_sum]['names'].append(flag)
embed.add_field(name='Flags', value='\n'.join([f'• `{"`, `".join(flag["names"])}` - {flag["help"].format(flag=flag["names"][0])}' for flag in all_flags.values()]), inline=False)
await ctx.send(embed=embed)
@commands.command(name='commands', usage='commands', description='Liste alle verfügbaren Befehle auf')
async def cmds(self, ctx: commands.Context):
commands = {command.name: command for command in self.bot.commands}
groups = {'default': []}
for command in sorted(commands):
if hasattr(command, 'group'):
if command.group not in groups:
groups[command.group] = []
groups[command.group].append(commands[command])
new_line = '\n'
embed = discord.Embed(title='Commands', description=f'Um Hilfe zu einem bestimmten Befehl zu bekommen, tippe `{self.bot.command_prefix}help [command]`',
color=discord.Color(0xff0000))
for group_name, group in groups.items():
embed.add_field(name=group_name, value=f'```• {f"{new_line}".join([self.bot.command_prefix + command.usage for command in group])}```', inline=False)
await ctx.send(embed=embed)
@commands.command(name='help', usage='help [command]', description='Zeigt Hilfe an')
async def help(self, ctx: commands.Context, command: Optional[str]):
if command:
if cmd := self.bot.get_command(command):
await self.show_help(ctx, cmd)
else:
embed = discord.Embed(title=f'{self.bot.user.name} help', description=f'Der Befehl {command} existiert nicht!', color=discord.Color(0xff0000))
embed.add_field(name='Get help', value=f'Um Hilfe zu einem bestimmten Befehl zu bekommen, tippe `{self.bot.command_prefix}help [command]`', inline=False)
embed.add_field(name='Commands', value=f'Tippe `{self.bot.command_prefix}commands`, um eine Liste mit allen Befehlen zu bekommen', inline=False)
embed.set_footer(text='<...> - required; [...] - optional; <...|...> - or\nDo NOT include <>, [] or | when executing the command')
await ctx.send(embed=embed)
else:
embed = discord.Embed(title=f'{self.bot.user.name} help', color=discord.Color(0xff0000))
embed.add_field(name='Get help', value=f'Um Hilfe zu einem bestimmten Befehl zu bekommen, tippe `{self.bot.command_prefix}help [command]`', inline=False)
embed.add_field(name='Commands', value=f'Tippe `{self.bot.command_prefix}commands`, um eine Liste mit allen Befehlen zu bekommen', inline=False)
embed.set_footer(text='<...> - required; [...] - optional; <...|...> - or\nDo NOT include <>, [] or | when executing the command')
await ctx.send(embed=embed)
class Info(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.flag_parser = flags.Flag()
@commands.command(name='info', usage='info [@user]', help='Zeigt Infos über einen Nutzer an')
@commands.guild_only()
async def info(self, ctx: commands.Context, *, user_mention: str = None):
id = ctx.author.id
if user_mention is not None:
regex_id = re.match(r'^<@(!)?(?P<id>\d{18})>', user_mention)
if regex_id:
id = regex_id.group('id')
else:
await Help(self.bot).show_help(ctx, ctx.command)
return
if (infos := self.bot.database.get_user_infos(id)) is None:
await ctx.send(embed=Embeds.error_embed(description="Der Nutzer existiert nicht"))
return
else:
member = await self.bot.guild.fetch_member(int(id))
name = member.display_name if member.display_name.lower().endswith(('s', 'z')) else member.display_name + "'s"
embed = discord.Embed(title=name + " Infos", color=member.roles[-1].color)
embed.set_thumbnail(url=member.avatar_url)
embed.set_footer(text='Tippe `$help addinfo` um zu sehen, was für Infos noch hinzugefügt / geändert werden können')
for key, value in infos.items():
if value is not None:
embed.add_field(name=key, value=value, inline=False)
if len(embed.fields) == 0:
embed.description = "Es wurden noch keine Infos eingetragen"
await ctx.send(embed=embed)
@commands.command(name='addinfo', aliases=['infoadd'], usage='addinfo <name, age | list | fav | waifu | husbando> <name, alter, link zu einer anime liste, lieblings anime, waifu, husbando>',
help='Fügt den Namen / das Alter / eine Anime Liste / einen Lieblings Anime / eine Waifu / ein Husbando zu einem Nutzer hinzu.\n\n'
'Tippe `$help removeinfo` um Infos wieder zu entfernen')
@commands.guild_only()
async def infoadd(self, ctx: commands.Context, info_type: str, *info_value):
if info_type.lower() == 'name':
self.bot.database.set_user_name(ctx.author.id, ' '.join(info_value))
await ctx.send(embed=Embeds.success_embed(description=f'Name (__{" ".join(info_value)}__) wurde hinzugefügt'))
elif info_type.lower() == 'age':
try:
age = int(info_value[0])
except ValueError:
await ctx.send(embed=Embeds.error_embed(description='`age` sollte eine Zahl sein'))
return
if age < 0:
await ctx.send(embed=Embeds.error_embed(description='Hmmm noch nie von einer Person gehört die minus Jahre alt ist'))
return
elif age > 99:
await ctx.send(embed=Embeds.error_embed(description='Uff, bei so einem hohen Alter komm selbst ich in Bedrängnis und kann es nicht zu deinen Infos hinzufügen :/'))
return
self.bot.database.set_user_age(ctx.author.id, age)
# ADDED AFTERWARDS: hehe
if age == 69:
embed = Embeds.success_embed(title='Alter wurde hinzugefügt')
embed.description = 'Ah, I see you\'re a man of culture as well'
embed.set_thumbnail(url='attachment://man_of_culture.jpg')
await ctx.send(embed=embed, file=discord.File(Path.cwd().joinpath('assets', 'man_of_culture.jpg')))
else:
await ctx.send(embed=Embeds.success_embed(description=f'Alter __{age}__ wurde hinzugefügt'))
elif info_type.lower() == 'list':
try:
requests.get(info_value[0])
except Exception:
await ctx.send(embed=Embeds.error_embed(description='Ich konnte mich nicht mit der gegeben URL verbinden'))
return
self.bot.database.set_user_list(ctx.author.id, info_value[0])
await ctx.send(embed=Embeds.success_embed(description='Anime Liste wurde hinzugefügt'))
elif info_type.lower() == 'fav':
self.bot.database.set_user_fav(ctx.author.id, ' '.join(info_value))
await ctx.send(embed=Embeds.success_embed(description=f'Lieblings Anime (__{" ".join(info_value)}__) wurde hinzugefügt'))
elif info_type.lower() == 'waifu':
self.bot.database.set_user_waifu(ctx.author.id, ' '.join(info_value))
await ctx.send(embed=Embeds.success_embed(description=f'Waifu (__{" ".join(info_value)}__) wurde hinzugefügt'))
elif info_type.lower() == 'husbando':
self.bot.database.set_user_husbando(ctx.author.id, ' '.join(info_value))
await ctx.send(embed=Embeds.success_embed(description=f'Husbando (__{" ".join(info_value)}__) wurde hinzugefügt'))
else:
await Help(self.bot).show_help(ctx, ctx.command)
@commands.command(name='removeinfo', aliases=['inforemove'], usage='removeinfo <name, age | list | fav | waifu | husbando>',
help='Entfernt Name / Alter / Anime Liste / lieblings Anime / Waifu / Husbando von einem Nutzer.\n\n'
'Tippe `$help addinfo` um Infos hinzuzufügen')
@commands.guild_only()
async def inforemove(self, ctx: commands.Context, info_type: str):
if info_type.lower() == 'name':
self.bot.database.set_user_name(ctx.author.id, None)
await ctx.send(embed=Embeds.success_embed(description='Name wurde entfernt'))
elif info_type.lower() == 'age':
self.bot.database.set_user_age(ctx.author.id, None)
await ctx.send(embed=Embeds.success_embed(description='Alter wurde entfernt'))
elif info_type.lower() == "list":
self.bot.database.set_user_list(ctx.author.id, None)
await ctx.send(embed=Embeds.success_embed(description='Anime Liste wurde entfernt'))
elif info_type.lower() == "fav":
self.bot.database.set_user_fav(ctx.author.id, None)
await ctx.send(embed=Embeds.success_embed(description='Lieblings Anime wurde entfernt'))
elif info_type.lower() == "waifu":
self.bot.database.set_user_waifu(ctx.author.id, None)
await ctx.send(embed=Embeds.success_embed(description='Waifu wurde entfernt'))
elif info_type.lower() == "husbando":
self.bot.database.set_user_husbando(ctx.author.id, None)
await ctx.send(embed=Embeds.success_embed(description='Husbando wurde entfernt'))
else:
await Help(self.bot).show_help(ctx, ctx.command)
class Kaizen(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command(name='kaizen', usage='kaizen', help='Alle Links zu den Social Media Kanälen von Kaizen')
async def links(self, ctx: commands.Context):
await Embeds.send_kaizen_infos(ctx)
class Vote(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.total_votes = []
self.next_normal_time = datetime.datetime.now()
self.sub_or_booster_time = {}
self.number_emojis = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '🔟']
self.flag_parser = flags.Flag(raise_errors=False,
double_flag_error='Die flag `{flag}` wurde schon gegeben',
flag_position_error='Flags müssen als letztes aufgerufen werden',
quotation_error='Es fehlen ein oder mehrere Anführungszeichen (`"` oder `\'`)')
self.flag_parser.add_flag('--duration',
store_type=(flags.StoreType.store_value, 60*5),
parser=self._duration_parser,
allowed_roles=['Mod', 'Server Booster', 'Kaizen-Sub'],
not_allowed_role_message='Nur Nutzer, die Server Booster oder Twich-Sub von Kaizen sind, können die `{flag}` flag benutzen!',
wrong_store_type_message='Die `{flag}` flag muss eine Zeitangabe haben (z.B. `{flag}=12m21s`)',
help='Zeit, bis das Ergebnis bekanntgegeben werden soll. Ist nur für Server Booster und Kaizen Twitch Subs verfügbar! (z.B. {flag}=12m21s)')
self.flag_parser.add_flag('--image',
store_type=flags.StoreType.store_value,
wrong_store_type_message='Die `{flag}` flag muss einen URL zu einem Bild haben (z.B. `{flag}="https://bytedream.org/darling.jpg"`)',
help='Fügt ein extra Bild zur Umfrage hinzu. Bild muss als URL angegeben sein (z.B. {flag}=https://bytedream.org/darling.jpg)')
super().__init__()
async def _duration_parser(self, ctx: commands.Context, flag, value):
regex_time = re.match(r'^((?P<hour>\d*?)h)?((?P<min>\d*?)m)?((?P<sec>\d*?)s)?', str(value))
time = 0
if regex_time['hour']:
time += int(regex_time['hour']) * 60 * 60
if regex_time['min']:
time += int(regex_time['min']) * 60
if regex_time['sec']:
time += int(regex_time['sec'])
if time == 0:
await ctx.send(embed=Embeds.warn_embed(description=f'Die Zeit der `{flag}` flag sollte in Form von __12m21s__ oder __21m__ sein'))
return False
elif time > 60 * 60:
roles = role_names(ctx.author)
if 'Mod' not in roles and 'A&M Redakteur' not in roles:
await ctx.send(embed=Embeds.warn_embed(description=f'Die Gesamtzeit der `{flag}` flag kann höchstens 60 Minuten betragen!'))
return False
return time
@commands.command(name='vote', ignore_extra=False,
usage='vote "<title>" <2 bis 10 "Antwortmöglichkeiten"> [flags]', help='Startet eine Umfrage mit maximal 10 Antwortmöglichkeiten',
flags='flag_parser')
@commands.guild_only()
async def vote(self, ctx: commands.Context, *, args):
parsed = await self.flag_parser.parse(args, ctx=ctx)
if not parsed:
return
args = list(parsed.normal_args)
if len(args) == 0:
async with ctx.typing():
await asyncio.sleep(.7)
await ctx.send(embed=Embeds.error_embed(description=f'Syntax für den \'{self.bot.command_prefix}vote\':\n'
f'`{self.bot.command_prefix}vote [titel] [antwortmöglichkeiten] (dauer)`\n\n'
f'Argumente:\n'
f'`titel`: Titel der Umfrage ("Wie findet ihr Anime xy?")\n'
f'`antwortmöglichkeiten`: Bis zu zehn mögliche Antwortmöglichkeiten ("super" "ok" "schlecht")\n'
f'`dauer`: Zeit, bis das Ergebnis bekanntgegeben werden soll.\n\n'
f'Beispiel:\n'
f'`{self.bot.command_prefix}vote "Wie findet Ihr Anime xy?" "sehr gut" super ok schlecht "sehr schlecht" time=12m`\n\n'
f'Um Leerzeichen im Titel oder in den Antwortmöglichkeiten zu benutzen, die entsprechenden Dinge einfach mit Anführungszeichen (`"`) vorne und hinten versehen'))
return
elif len(args) <= 2:
await ctx.send(embed=Embeds.error_embed('Es müssen mindestens zwei Antwortmöglichkeit angegeben sein!'))
return
elif len(args) > 10 + 1: # the +1 is the title
await ctx.send(embed=Embeds.error_embed('Es können nur maximal 10 Antwortmöglichkeiten angegeben werden!'))
return
roles = role_names(ctx.author)
privileges = 'Mod' in roles or 'A&M Redakteur' in roles
booster = 'Server Booster' in roles
sub = 'Kaizen-Sub' in roles
now = datetime.datetime.now()
if len(self.total_votes) >= 3 and not privileges:
difference = divmod(min(self.total_votes) - now.timestamp(), 60)
await ctx.send(embed=Embeds.error_embed(description=f'Es können maximal 3 Umfragen gleichzeitig laufen. In {round(difference[0])} Minuten und {round(difference[1])} Sekunden kann wieder eine neue Umfrage gestartet werden.'))
return
# check for cooldown
if privileges:
pass
elif booster or sub:
if self.sub_or_booster_time.get(ctx.author.id, now - datetime.timedelta(hours=1)) < now:
self.sub_or_booster_time[ctx.author.id] = now + datetime.timedelta(minutes=15)
elif self.next_normal_time < now:
self.next_normal_time = now + datetime.timedelta(minutes=40)
elif self.bot.database.set_user_extra_vote(ctx.author.id, -1):
pass
else:
if (t := self.sub_or_booster_time[ctx.author.id]) < self.next_normal_time:
difference = divmod((t - now).total_seconds(), 60)
else:
difference = divmod((self.next_normal_time - now).total_seconds(), 60)
await ctx.send(embed=Embeds.error_embed(description=f'Du musst leider noch {round(difference[0])} Minuten und {round(difference[1])} Sekunden warten, bis du den Befehl erneut benutzen kannst.'))
return
elif self.next_normal_time < now:
self.next_normal_time = now + datetime.timedelta(minutes=40)
else:
difference = divmod((self.next_normal_time - now).total_seconds(), 60)
await ctx.send(embed=Embeds.error_embed(description=f'Du musst leider noch {round(difference[0])} Minuten und {round(difference[1])} Sekunden warten, du den Befehl erneut benutzen kannst.\n'
f'Werde Server Booster oder Twich-Sub, um die Wartezeit zu verringern!'))
return
# send the message embed
args[0] = args[0] if args[0].endswith('?') else args[0] + '?'
embed = discord.Embed(title=f'**{args[0]}**',
description='\n'.join([f'{self.number_emojis[i]}: {answer}' for i, answer in enumerate(args[1:])]),
color=ctx.author.roles[-1].color)
if parsed.image:
embed.set_thumbnail(url=parsed.image)
end_time = datetime.datetime.now() + datetime.timedelta(seconds=parsed.duration)
embed.set_footer(text=f'Umfrage endet am {end_time.strftime("%d.%m.%Y")} um ca. {end_time.strftime("%H:%M")} Uhr')
async with ctx.typing():
await asyncio.sleep(.5)
message = await ctx.send(embed=embed)
for i in range(len(args[1:])):
await message.add_reaction(self.number_emojis[i])
if parsed.pin:
await message.pin()
await ctx.message.delete()
last_mention_id = message.id
self.total_votes.append(end_time.timestamp())
walked = 0
for _ in range(0, (parsed.duration - 5) // (60 * 10)):
await asyncio.sleep(60 * 10)
is_fresh = False
async for old_message in ctx.channel.history(limit=10):
if old_message.id == last_mention_id:
is_fresh = True
break
if not is_fresh:
last_mention_id = (await message.reply(embed=discord.Embed(description=f'Es läuft aktuell eine Umfrage, stimmt doch zur Frage `{args[0]}` ab!',
color=embed.color))).id
walked += 60 * 10
await asyncio.sleep(parsed.duration - walked)
self.total_votes.remove(end_time.timestamp())
if parsed.pin:
await message.unpin()
reactions = []
users = []
for reaction in (await ctx.channel.fetch_message(message.id)).reactions:
reactions.append(reaction)
async for user in reaction.users():
if not user.bot and user.id not in users:
users.append(user.id)
embed = discord.Embed(title=f'**{args[0]}**',
description='\n'.join([f'{self.number_emojis[i]}: {answer} - {reactions[i].count - 1} {"Stimme" if reactions[i].count - 1 == 1 else "Stimmen"}' for i, answer in enumerate(args[1:])]),
color=embed.color)
if parsed.image:
embed.set_thumbnail(url=parsed.image)
now = datetime.datetime.now()
embed.set_footer(text=f'Umfrage endete am {now.strftime("%d.%m.%Y")} um {now.strftime("%H:%M:%S")} Uhr')
await message.clear_reactions()
await message.edit(embed=embed)
reaction_dict = {arg: count for arg, count in sorted({arg: reactions[i].count for i, arg in enumerate(args[1:])}.items(), key=lambda item: item[1], reverse=True)}
result_embed = discord.Embed(title=f'Umfrageergebnisse zu `{args[0]}`\n\n',
description='\n'.join([f'{i + 1}. {arg} - {count - 1} {"Stimme" if count - 1 == 1 else "Stimmen"}' for i, (arg, count) in enumerate(reaction_dict.items())]),
color=embed.color)
result_embed.description += f'\n\nInsgesamt mitgemacht: {len(users)}'
await ctx.send(embed=result_embed)
# ADDED AFTERWARDS: hehe pt. 2
class NSFW(commands.Cog):
def __init__(self, bot):
self.id = 796873618026004481
self.bot = bot
@commands.command(name='color', usage='color', help='Shows a random colored hentai image')
@commands.guild_only()
@commands.cooldown(1, 60 * 5, type=commands.BucketType.channel)
async def color(self, ctx: commands.Context):
if ctx.channel.id != self.id:
await ctx.send(embed=Embeds.error_embed(description='Dieser Befehl kann nur in <#796873618026004481> genutzt werden'))
else:
await ctx.send(file=discord.File(os.path.join('/srv/media/hentai/image', random.choice(os.listdir('/srv/media/hentai/image')))))
@commands.command(name='lewd', usage='lewd', help='Shows a random lewd')
@commands.guild_only()
@commands.cooldown(1, 60 * 5, type=commands.BucketType.channel)
async def lewd(self, ctx: commands.Context):
if ctx.channel.id != self.id:
await ctx.send(embed=Embeds.error_embed(description='Dieser Befehl kann nur in <#796873618026004481> genutzt werden'))
else:
page = random.randint(0, glob['twitter'].get_user('lewdxsthetic').statuses_count // 20)
await ctx.send(random.choice(glob['twitter'].user_timeline('lewdxsthetic', page=page, count=20))._json['entities']['media'][0]['media_url_https'])

203
kaizenbot/database.py Normal file
View File

@ -0,0 +1,203 @@
import mariadb
import datetime
from typing import Any, Dict, List, Union
import discord
from .user import User
class Database:
def __init__(self, user: str, password: str):
self.database: mariadb._mariadb.connection = mariadb.connect(user=user, password=password, host='127.0.0.1', port=3306, database='kaizen')
self.database.autocommit = True
self.cursor = self.database.cursor()
def sync(self):
self.cursor.execute('INSERT INTO info (id) (SELECT id FROM rules_accepted WHERE id NOT IN (SELECT id FROM info))')
self.cursor.execute('INSERT INTO economy (id) (SELECT id FROM rules_accepted WHERE id NOT IN (SELECT id FROM economy))')
# --- #
def get_message_id(self) -> Union[int, None]:
try:
self.cursor.execute('SELECT v FROM stuff WHERE k=?', ('message_id',))
return int(self.cursor.fetchone()[0])
except (TypeError, ValueError):
return None
def set_message_id(self, id: int):
if id is None:
self.cursor.execute('DELETE FROM stuff WHERE k=?', ('message_id',))
else:
self.cursor.execute('INSERT INTO stuff (k, v) VALUES (?, ?)', ('message_id', str(id)))
def get_image_id(self) -> Union[int, None]:
try:
self.cursor.execute('SELECT v FROM stuff WHERE k=?', ('image_id',))
return int(self.cursor.fetchone()[0])
except (TypeError, ValueError):
return None
def set_image_id(self, id: int):
if id is None:
self.cursor.execute('DELETE FROM stuff WHERE k=?', ('image_id',))
else:
self.cursor.execute('INSERT INTO stuff (k, v) VALUES (?, ?)', ('image_id', str(id)))
# --- user specific --- #
"""def user(self, member: discord.Member) -> Union[User, None]:
self.cursor.execute('SELECT * FROM rules_accepted WHERE id=?', (member.id,))
result = self.cursor.fetchone()
if result:
return User(result[0], result[1], result[2], member.joined_at, result[3])
else:
self.cursor.execute('SELECT * FROM normal_user WHERE id=?', (member.id,))
result = self.cursor.fetchone()
if result:
return User(result[0], result[1], result[2], result[3], None, result[4], result[5])
else:
return None"""
def add_user(self, user: discord.Member, join_message_id: int) -> User:
self.cursor.execute('INSERT INTO normal_user (id, name, tag, joined, join_message) VALUES (?, ?, ?, ?, ?)', (user.id, user.display_name, str(user), user.joined_at, join_message_id))
return User(user.id, user.display_name, str(user), user.joined_at, join_message=join_message_id)
def get_all_users(self) -> Dict[int, User]:
users = {}
self.cursor.execute('SELECT * FROM normal_user')
for row in self.cursor.fetchall():
users[row[0]] = User(row[0], row[1], row[2], row[3], None, row[4], row[5])
self.cursor.execute('SELECT * FROM rules_accepted')
for row in self.cursor.fetchall():
users[row[0]] = User(row[0], row[1], row[2], None, row[3])
return users
def set_user_warning(self, user: User, warning_time: datetime.datetime):
self.cursor.execute('UPDATE normal_user SET warned=? WHERE id=?', (warning_time, user.id))
user.warning_time = warning_time
def add_user_accepted_rules(self, user: User, accepted_rules_datetime: datetime.datetime):
self.cursor.execute('DELETE FROM normal_user WHERE id=?', (user.id,))
self.cursor.execute('INSERT INTO rules_accepted (id, name, tag, accepted) VALUES (?, ?, ?, ?)', (user.id, user.name, user.tag, accepted_rules_datetime))
self.cursor.execute('INSERT INTO info (id) VALUES (?)', (user.id,))
self.cursor.execute('INSERT INTO economy (id) SELECT id FROM rules_accepted WHERE NOT EXISTS(SELECT id FROM rules_accepted WHERE id=?)', (user.id,))
user.warning_time = None
user.accepted_rules_date = accepted_rules_datetime
user.join_message = None
def reset_user(self, user: User):
self.cursor.execute('DELETE FROM rules_accepted WHERE id=?', (user.id,))
now = datetime.datetime.now()
self.cursor.execute('INSERT INTO normal_user (id, name, tag, joined) VALUES (?, ?, ?, ?)', (user.id, user.name, user.tag, now))
user.joined = now
user.warning_time = None
user.accepted_rules_date = None
def remove_user(self, user: Union[User, int]):
if isinstance(user, User):
id = user.id
else:
id = user
self.cursor.execute('DELETE FROM normal_user WHERE id=?', (id,))
self.cursor.execute('DELETE FROM rules_accepted WHERE id=?', (id,))
self.cursor.execute('DELETE FROM info WHERE id=?', (id,))
def change_user_infos(self, user: User, new_name: str, new_tag: str):
self.cursor.execute('UPDATE normal_user SET name=?, tag=? WHERE id=?', (new_name, new_tag, user.id))
self.cursor.execute('UPDATE rules_accepted SET name=?, tag=? WHERE id=?', (new_name, new_tag, user.id))
user.name = new_name
user.tag = new_tag
# --- user infos --- #
def get_user_infos(self, id: int) -> Union[Dict[str, Any], None]:
self.cursor.execute('SELECT * FROM info WHERE id=?', (id,))
result = self.cursor.fetchone()
try:
return {
'Name': result[1],
'Alter': result[2],
'Anime Liste': result[3],
'Lieblings Anime': result[4],
'Waifu': result[5],
'Husbando': result[6],
}
except TypeError:
return None
def set_user_name(self, id: int, name: str):
self.cursor.execute('UPDATE info SET name=? WHERE id=?', (name, id))
def set_user_age(self, id: int, age: [int, None]):
self.cursor.execute('UPDATE info SET age=? WHERE id=?', (age, id))
def set_user_list(self, id: int, list: Union[str, None]):
self.cursor.execute('UPDATE info SET list=? WHERE id=?', (list, id))
def set_user_fav(self, id: int, fav: str):
self.cursor.execute('UPDATE info SET fav=? WHERE id=?', (fav, id))
def set_user_waifu(self, id: int, waifu: str):
self.cursor.execute('UPDATE info SET waifu=? WHERE id=?', (waifu, id))
def set_user_husbando(self, id: int, husbando: str):
self.cursor.execute('UPDATE info SET husbando=? WHERE id=?', (husbando, id))
# --- points --- #
def add_user_gold(self, id: int, gold: int):
self.cursor.execute('UPDATE economy SET gold=gold + ? WHERE id=?', (gold, id))
def has_remove_user_gold(self, id: int, gold: int) -> bool:
self.cursor.execute('UPDATE economy SET gold=gold - ? WHERE id=? AND gold >= ?', (gold, id, gold))
return self.cursor.rowcount > 0
def get_user_items(self, id: int) -> Dict[str, int]:
self.cursor.execute('SELECT * FROM economy WHERE id=?', (id,))
result = self.cursor.fetchone()
return {
'gold': result[1],
'extra vote': result[2],
'color': result[3]
}
def get_leaderboard(self) -> Dict[str, int]:
self.cursor.execute('SELECT id, gold FROM economy ORDER BY gold DESC LIMIT 10')
return {user[0]: user[1] for user in self.cursor.fetchall()}
def set_user_extra_vote(self, id: int, count: int):
self.cursor.execute('UPDATE economy SET extra_vote=extra_vote + ? WHERE id=? AND extra_vote > 0', (count, id))
return self.cursor.rowcount > 0
def set_user_color_count(self, id: int, count: int):
self.cursor.execute('UPDATE economy SET color=color + ? WHERE id=?', (count, id))
def set_user_color(self, id: int, color: str):
self.cursor.execute('UPDATE info SET color=? WHERE id=?', (color, id))
# --- family --- #
def add_user_parent(self, user: int, parent_id: int):
self.cursor.execute('INSERT INTO family (id, parent) SELECT ?, ? WHERE NOT EXISTS(SELECT * FROM family WHERE id=? AND parent=?)', (user, parent_id, user, parent_id))
def remove_user_parent(self, user: int, parent_id: int):
self.cursor.execute('DELETE FROM family WHERE id=? AND parent=?', (user, parent_id))
def get_user_parents(self, user: int) -> List[int]:
parents = []
self.cursor.execute('SELECT parent FROM family WHERE id=?', (user,))
for parent in self.cursor.fetchall:
parents.append(parent[0])
return parents
def get_user_children(self, user: int) -> List[int]:
children = []
self.cursor.execute('SELECT id FROM family WHERE parent=?', (user,))
for parent in self.cursor.fetchall:
children.append(parent[0])
return children

295
kaizenbot/flags.py Normal file
View File

@ -0,0 +1,295 @@
from copy import copy
from discord.ext import commands
from enum import Enum
import shlex
import typing
from .utils import Embeds, role_names
class AlternativeStoreType:
class _StoreType:
pass
class Bool(_StoreType):
pass
class Int(_StoreType):
def __init__(self, min: int = float('-inf'), max: int = float('inf')):
if min > max:
raise ValueError('min must be higher than max')
self.min = min
self.max = max
class Value(_StoreType):
pass
store_bool = Bool()
store_int = Int()
store_value = Value()
class StoreType(Enum):
store_bool = 'store_bool'
store_value = 'store_value'
class _Parsed:
def __init__(self):
self.normal_args = ()
self.ctx: commands.Context = None
class FlagErrors:
class _FlagError(Exception):
def __init__(self, message: str = None, flag: str = None):
self.flag = flag
super().__init__(message)
class DoubleFlagError(_FlagError):
pass
class FlagParseError(_FlagError):
pass
class FlagPositionError(_FlagError):
def __init__(self, message: str = None, flag: str = None):
super().__init__(message, flag)
class FlagStoreError(_FlagError):
def __init__(self, message: str = None, flag: str = None, store_type: StoreType = None, value=None):
self.store_type = store_type
self.value = value
super().__init__(message, flag)
class RoleNotAllowedError(_FlagError):
def __init__(self, message: str = None, flag: str = None, role: str = None):
self.role = role
super().__init__(message, flag)
class QuotationError(Exception):
def __init__(self, message: str = None):
super().__init__(message)
class _ParsedFlag:
def __init__(self, flag: str, value):
self.flag = flag
self._value = value
def __new__(cls, flag: str, value):
return value
def __add__(self, other):
return self._value + other
def __bool__(self):
return True if self._value else False
def __ge__(self, other):
return self._value >= other
def __getitem__(self, item):
return self._value
def __gt__(self, other):
return self._value < other
def __hash__(self):
return hash((self.flag, self._value))
def __le__(self, other):
return self._value <= other
def __len__(self):
return len(self._value)
def __lt__(self, other):
return self._value < other
def __repr__(self):
return self._value
class Flag:
def __init__(self, raise_errors=True,
double_flag_error='The flag `{flag}` was already given',
flag_position_error='Flags must be called at the end of a command',
quotation_error='At least one quotation mark is missing (`"` or `\'`)'):
self.double_flag_error = double_flag_error
self.raise_errors = raise_errors
self.flag_position_error = flag_position_error
self.quotation_error = quotation_error
self._flags = {}
def add_flag(self, *flags,
store_type: typing.Union[StoreType, typing.Tuple[StoreType, typing.Any]],
parser: typing.Callable = None,
allowed_roles: typing.Union[typing.List[str], typing.Tuple[str]] = [],
disallowed_roles: typing.Union[typing.List[str], typing.Tuple[str], typing.Dict[str, str]] = [],
not_allowed_role_message='User has no allowed role for flag {flag}',
wrong_store_type_message=None,
help: str = '', show_help=True):
if store_type == StoreType.store_bool and parser is not None:
raise FlagErrors.FlagParseError('The flag parser cannot be set if the store type is \'store_bool\'')
if store_type == StoreType.store_bool:
default_value = False
else:
default_value = None
for allowed in allowed_roles:
if allowed in disallowed_roles:
raise ValueError(f'Role `{allowed}` cannot be allowed and disallowed at the same time')
flag_information = {'store_type': store_type[0] if isinstance(store_type, tuple) else store_type, 'default': store_type[1] if isinstance(store_type, tuple) else default_value,
'parser': parser,
'allowed_roles': allowed_roles, 'disallowed_roles': disallowed_roles,
'not_allowed_role_message': not_allowed_role_message,
'wrong_store_type_message': wrong_store_type_message,
'help': help, 'show_help': show_help}
for flag in flags:
flag = str(flag)
self._flags[flag] = flag_information
def flags(self) -> typing.Dict[str, typing.Any]:
return copy(self._flags)
async def parse(self, args: str, ctx: commands.Context):
# (re)sets the attributes every time
parsed = _Parsed()
try:
shlex_args = shlex.split(args)
except ValueError as error:
if str(error) == 'No closing quotation':
if self.raise_errors:
raise FlagErrors.QuotationError(self.quotation_error)
else:
await ctx.send(embed=Embeds.error_embed(description=self.quotation_error))
return
else:
raise error
for flag, information in self._flags.items():
parsed.__setattr__(flag[2:], information['default'])
flag_indexed = False
normal_args = []
parsed_flags = []
roles = role_names(ctx.author)
for i, arg in enumerate(shlex_args):
arg = str(arg).replace('"', '').replace("'", '')
if '=' in arg:
arg, value = arg.split('=', 1)
else:
value = None
if arg in self._flags:
if arg in parsed_flags:
if self.raise_errors:
raise FlagErrors.DoubleFlagError(self.double_flag_error.format(flag=arg))
else:
await ctx.send(embed=Embeds.error_embed(description=self.double_flag_error.format(flag=arg)))
return
else:
parsed_flags.append(arg)
if not flag_indexed:
flag_indexed = True
flag = self._flags[arg]
# --- #
allowed_roles = flag['allowed_roles']
if allowed_roles:
if not any(allowed in roles for allowed in allowed_roles):
error = flag['not_allowed_role_message'].format(flag=arg)
if self.raise_errors:
raise FlagErrors.RoleNotAllowedError(error)
else:
await ctx.send(embed=Embeds.error_embed(description=error))
return
disallowed_roles = flag['disallowed_roles']
if disallowed_roles:
for disallowed in disallowed_roles:
if disallowed in roles:
error = disallowed_roles[disallowed] if isinstance(disallowed_roles, dict) else 'The role `{role}` is not allowed to use the {flag} flag'
error = error.format(role=disallowed, flag=arg)
if self.raise_errors:
raise FlagErrors.RoleNotAllowedError(message=error, flag=arg, role=disallowed)
else:
await ctx.send(embed=Embeds.error_embed(description=error))
return
store_type = flag['store_type']
arg_without_prefix = arg[2:]
if store_type == StoreType.store_bool:
error = flag['wrong_store_type_message'] if flag['wrong_store_type_message'] else 'Flag `{flag}` must not contain a value'
error = error.format(flag=arg)
if value:
if self.raise_errors:
raise FlagErrors.FlagStoreError(message=error, flag=arg, store_type=store_type, value=value)
else:
await ctx.send(embed=Embeds.error_embed(description=error))
return
else:
parsed.__setattr__(arg_without_prefix, _ParsedFlag(flag, True))
elif store_type == StoreType.store_value:
error = flag['wrong_store_type_message'] if flag['wrong_store_type_message'] else 'Flag `{flag}` must not contain a value'
error = error.format(flag=arg)
if not value:
if self.raise_errors:
raise FlagErrors.FlagStoreError(message=error, flag=arg, store_type=store_type)
else:
await ctx.send(embed=Embeds.error_embed(description=error))
return
else:
if parser := flag['parser']:
value_parsed = await parser(ctx, arg, value)
if isinstance(value_parsed, bool):
if value_parsed:
parsed.__setattr__(arg_without_prefix, _ParsedFlag(flag, True))
else:
return
else:
parsed.__setattr__(arg_without_prefix, _ParsedFlag(flag, value_parsed))
else:
parsed.__setattr__(arg_without_prefix, _ParsedFlag(flag, value))
elif flag_indexed:
if self.raise_errors:
raise FlagErrors.FlagPositionError(message=self.flag_position_error)
else:
await ctx.send(embed=Embeds.error_embed(description=self.flag_position_error))
return
else:
normal_args.append(arg)
parsed.normal_args = tuple(normal_args)
parsed.ctx = ctx
return parsed
def get_flags(command: commands.Command) -> typing.Union[Flag, None]:
flags = command.__original_kwargs__.get('flags', None)
if isinstance(flags, str):
try:
return command.cog.__getattribute__(flags)
except AttributeError:
raise AttributeError(f'The flag `{flags}` does not exist')
elif isinstance(flags, Flag):
return flags
else:
return None

13
kaizenbot/user.py Normal file
View File

@ -0,0 +1,13 @@
import datetime
class User:
def __init__(self, id: int, name: str, tag: str, joined: datetime.datetime, accepted_rules_date: datetime.datetime = None, warning_time: datetime.datetime = None, join_message = None):
self.id = id
self.name = name
self.tag = tag
self.joined = joined
self.accepted_rules_date = accepted_rules_date
self.warning_time = warning_time
self.join_message = join_message

476
kaizenbot/utils.py Normal file
View File

@ -0,0 +1,476 @@
import asyncio
import random
import typing
from pathlib import Path
from threading import Timer as _Timer
from time import sleep
import discord
from discord.ext import menus
from . import logger
class AsyncTimer:
def __init__(self, start: float, callback, *args):
self._callback = callback
self._args = args
self._start = start
self._task = asyncio.ensure_future(self._job())
async def _job(self):
await asyncio.sleep(self._start)
await self._callback(*self._args)
def cancel(self):
self._task.cancel()
class AsyncIntervalTimer(AsyncTimer):
def __init__(self, first_start: float, interval: float, callback, *args):
super().__init__(first_start, callback, *args)
self._interval = interval
async def _job(self):
await super()._job()
while True:
await asyncio.sleep(self._interval)
await self._callback(*self._args)
def cancel(self):
self._task.cancel()
class IntervalTimer:
def __init__(self, first_start: float, interval: float, func, *args):
self.first_start = first_start
self.interval = interval
self.handlerFunction = func
self.args = args
self.running = False
self.timer = _Timer(self.interval, self.run, args)
def run(self, *args):
sleep(self.first_start)
self.handlerFunction(*args)
while self.running:
sleep(self.interval)
self.handlerFunction(*args)
def start(self):
self.running = True
self.timer.start()
def cancel(self):
self.running = False
pass
class Embeds:
@staticmethod
async def send_kaizen_infos(channel):
file = discord.File(Path.cwd().joinpath('assets', 'kaizen-round.png'))
embed = discord.Embed(title='**Kaizen**', description='Folge Kaizen auf den folgenden Kanälen, um nichts mehr zu verpassen!', color=discord.Color(0xff0000))
embed.set_thumbnail(url='attachment://kaizen-round.png')
embed.add_field(name='**🎥Youtube Hauptkanal**', value='Abonniere Kaizen auf __**[Youtube](https://www.youtube.com/c/KaizenAnime)**__ um kein Anime Video mehr zu verpassen!', inline=False)
embed.add_field(name='**📑Youtube Toplisten-Kanal**', value='Abonniere Kaizen\'s __**[Toplisten-Kanal](https://www.youtube.com/channel/UCoijG8JqKb1rRZofx5b-LCw)**__ um kein Toplisten-Video mehr zu verpassen!', inline=False)
embed.add_field(name='**📯Youtube Stream-Clips & mehr**', value='Abonniere Kaizen\'s __**[Youtube Kanal](https://www.youtube.com/channel/UCodeTj8SJ-5HhJgC_Elr1Dw)**__ für Stream-Clips & mehr!', inline=False)
embed.add_field(name='**📲Twitch**', value='Folge Kaizen auf __**[Twitch](https://www.twitch.tv/kaizenanime)**__ und verpasse keinen Stream mehr! '
'Subbe Kaizen um eine exklusive Rolle auf dem Discord Server zu bekommen!', inline=False)
embed.add_field(name='**📢Twitter**', value='Folge Kaizen auf __**[Twitter](https://twitter.com/Kaizen_Anime)**__ um aktuelle Informationen zu bekommen und in Videos / Streams mitzuwirken!', inline=False)
embed.add_field(name='**📷Instagram**', value='Folge Kaizen auf __**[Instagram](https://www.instagram.com/kaizen.animeyt/)**__!', inline=False)
await channel.send(embed=embed, file=file)
@staticmethod
def error_embed(title: typing.Union[str, None] = None, description: typing.Union[str, None] = None) -> discord.Embed:
embed = discord.Embed(color=discord.Color(0xff0000))
if title:
embed.title = title
if description:
embed.description = description
return embed
@staticmethod
def warn_embed(title: typing.Union[str, None] = None, description: typing.Union[str, None] = None) -> discord.Embed:
embed = discord.Embed(color=discord.Color(0xff9055))
if title:
embed.title = title
if description:
embed.description = description
return embed
@staticmethod
def success_embed(title: typing.Union[str, None] = None, description: typing.Union[str, None] = None) -> discord.Embed:
embed = discord.Embed(color=discord.Color(0x00ff00))
if title:
embed.title = title
if description:
embed.description = description
return embed
class MenuListPageSource(menus.ListPageSource):
def __init__(self, data):
super().__init__(data, per_page=1)
async def format_page(self, menu, embeds):
return embeds
def random_sequence_not_in_string(string: str):
sequence = '+'
while sequence in string:
choice = random.choice('+*~-:%&')
sequence = choice + sequence + choice
return sequence
def role_names(member: discord.Member) -> typing.List[str]:
return [role.name for role in member.roles]
# ADDED AFTERWARDS: I've stol- copied the following code from a tweepy (https://github.com/tweepy/tweepy) PR or gist (from where exactly I do not know anymore lul)
# at the time when they didn't support async actions
# Tweepy
# Copyright 2009-2021 Joshua Roesslein
# See LICENSE for details.
import json
from math import inf
from platform import python_version
import aiohttp
from oauthlib.oauth1 import Client as OAuthClient
from yarl import URL
import tweepy
from tweepy.error import TweepError
from tweepy.models import Status
class AsyncStream:
"""Stream realtime Tweets asynchronously
Parameters
----------
consumer_key: :class:`str`
Consumer key
consumer_secret: :class:`str`
Consuemr secret
access_token: :class:`str`
Access token
access_token_secret: :class:`str`
Access token secret
max_retries: Optional[:class:`int`]
Number of times to attempt to (re)connect the stream.
Defaults to infinite.
proxy: Optional[:class:`str`]
Proxy URL
"""
def __init__(self, consumer_key, consumer_secret, access_token,
access_token_secret, max_retries=inf, proxy=None):
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
self.access_token = access_token
self.access_token_secret = access_token_secret
self.max_retries = max_retries
self.proxy = proxy
self.session = None
self.task = None
self.user_agent = (
f"Python/{python_version()} "
f"aiohttp/{aiohttp.__version__} "
f"Tweepy/{tweepy.__version__}"
)
async def _connect(self, method, endpoint, params={}, headers=None,
body=None):
error_count = 0
# https://developer.twitter.com/en/docs/twitter-api/v1/tweets/filter-realtime/guides/connecting
stall_timeout = 90
network_error_wait = network_error_wait_step = 0.25
network_error_wait_max = 16
http_error_wait = http_error_wait_start = 5
http_error_wait_max = 320
http_420_error_wait_start = 60
oauth_client = OAuthClient(self.consumer_key, self.consumer_secret,
self.access_token, self.access_token_secret)
if self.session is None or self.session.closed:
self.session = aiohttp.ClientSession(
headers={"User-Agent": self.user_agent},
timeout=aiohttp.ClientTimeout(sock_read=stall_timeout)
)
url = f"https://stream.twitter.com/1.1/{endpoint}.json"
url = str(URL(url).with_query(sorted(params.items())))
try:
while error_count <= self.max_retries:
request_url, request_headers, request_body = oauth_client.sign(
url, method, body, headers
)
try:
async with self.session.request(
method, request_url, headers=request_headers,
data=request_body, proxy=self.proxy
) as resp:
if resp.status == 200:
error_count = 0
http_error_wait = http_error_wait_start
network_error_wait = network_error_wait_step
await self.on_connect()
async for line in resp.content:
line = line.strip()
if line:
await self.on_data(line)
else:
await self.on_keep_alive()
await self.on_closed(resp)
else:
await self.on_request_error(resp.status)
error_count += 1
if resp.status == 420:
if http_error_wait < http_420_error_wait_start:
http_error_wait = http_420_error_wait_start
await asyncio.sleep(http_error_wait)
http_error_wait *= 2
if resp.status != 420:
if http_error_wait > http_error_wait_max:
http_error_wait = http_error_wait_max
except (aiohttp.ClientConnectionError,
aiohttp.ClientPayloadError) as e:
await self.on_connection_error()
await asyncio.sleep(network_error_wait)
network_error_wait += network_error_wait_step
if network_error_wait > network_error_wait_max:
network_error_wait = network_error_wait_max
except asyncio.CancelledError:
return
except Exception as e:
await self.on_exception(e)
finally:
await self.session.close()
await self.on_disconnect()
async def filter(self, follow=None, track=None, locations=None,
stall_warnings=False):
"""This method is a coroutine.
Filter realtime Tweets
https://developer.twitter.com/en/docs/twitter-api/v1/tweets/filter-realtime/api-reference/post-statuses-filter
Parameters
----------
follow: Optional[List[Union[:class:`int`, :class:`str`]]]
A list of user IDs, indicating the users to return statuses for in
the stream. See https://developer.twitter.com/en/docs/twitter-api/v1/tweets/filter-realtime/guides/basic-stream-parameters
for more information.
track: Optional[List[:class:`str`]]
Keywords to track. Phrases of keywords are specified by a list. See
https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/basic-stream-parameters
for more information.
locations: Optional[List[:class:`float`]]
Specifies a set of bounding boxes to track. See
https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/basic-stream-parameters
for more information.
stall_warnings: Optional[:class:`bool`]
Specifies whether stall warnings should be delivered. See
https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/basic-stream-parameters
for more information. Def
logger = logging.getLogger('kaizen')aults to False.
Returns :class:`asyncio.Task`
"""
if self.task is not None and not self.task.done():
raise TweepError("Stream is already connected")
endpoint = "statuses/filter"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
body = {}
if follow is not None:
body["follow"] = ','.join(map(str, follow))
if track is not None:
body["track"] = ','.join(map(str, track))
if locations is not None:
if len(locations) % 4:
raise TweepError(
"Number of location coordinates should be a multiple of 4"
)
body["locations"] = ','.join(
f"{location:.4f}" for location in locations
)
if stall_warnings:
body["stall_warnings"] = "true"
self.task = asyncio.create_task(
self._connect("POST", endpoint, headers=headers, body=body or None)
)
return self.task
async def sample(self, stall_warnings=False):
"""This method is a coroutine.
Sample realtime Tweets
https://developer.twitter.com/en/docs/twitter-api/v1/tweets/sample-realtime/api-reference/get-statuses-sample
Parameters
----------
stall_warnings: Optional[:class:`bool`]
Specifies whether stall warnings should be delivered. See
https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/basic-stream-parameters
for more information. Defaults to False.
Returns :class:`asyncio.Task`
"""
if self.task is not None and not self.task.done():
raise TweepError("Stream is already connected")
endpoint = "statuses/sample"
params = {}
if stall_warnings:
params["stall_warnings"] = "true"
self.task = asyncio.create_task(
self._connect("GET", endpoint, params=params)
)
return self.task
def disconnect(self):
"""Disconnect the stream"""
if self.task is not None:
self.task.cancel()
async def on_closed(self, resp):
"""This method is a coroutine.
This is called when the stream has been closed by Twitter.
"""
logger.error("Stream connection closed by Twitter")
async def on_connect(self):
"""This method is a coroutine.
This is called after successfully connecting to the streaming API.
"""
# logger.info("Stream connected")
async def on_connection_error(self):
"""This method is a coroutine.
This is called when the stream connection errors or times out.
"""
# logger.error("Stream connection has errored or timed out")
async def on_disconnect(self):
"""This method is a coroutine.
This is called when the stream has disconnected.
"""
# logger.info("Stream disconnected")
async def on_exception(self, exception):
"""This method is a coroutine.
This is called when an unhandled exception occurs.
"""
logger.exception("Stream encountered an exception")
async def on_keep_alive(self):
"""This method is a coroutine.
This is called when a keep-alive message is received.
"""
#logger.debug("Received keep-alive message")
async def on_request_error(self, status_code):
"""This method is a coroutine.
This is called when a non-200 HTTP status code is encountered.
"""
# logger.error("Stream encountered HTTP Error: %d", status_code)
async def on_data(self, raw_data):
"""This method is a coroutine.
This is called when raw data is received from the stream.
This method handles sending the data to other methods, depending on the
message type.
https://developer.twitter.com/en/docs/twitter-api/v1/tweets/filter-realtime/guides/streaming-message-types
"""
data = json.loads(raw_data)
if "in_reply_to_status_id" in data:
status = Status.parse(None, data)
return await self.on_status(status)
if "delete" in data:
delete = data["delete"]["status"]
return await self.on_delete(delete["id"], delete["user_id"])
if "disconnect" in data:
return await self.on_disconnect_message(data["disconnect"])
if "limit" in data:
return await self.on_limit(data["limit"]["track"])
if "scrub_geo" in data:
return await self.on_scrub_geo(data["scrub_geo"])
if "status_withheld" in data:
return await self.on_status_withheld(data["status_withheld"])
if "user_withheld" in data:
return await self.on_user_withheld(data["user_withheld"])
if "warning" in data:
return await self.on_warning(data["warning"])
logger.warning("Received unknown message type: %s", raw_data)
async def on_status(self, status):
"""This method is a coroutine.
This is called when a status is received.
"""
# logger.debug("Received status: %d", status.id)
async def on_delete(self, status_id, user_id):
"""This method is a coroutine.
This is called when a status deletion notice is received.
"""
# logger.debug("Received status deletion notice: %d", status_id)
async def on_disconnect_message(self, message):
"""This method is a coroutine.
This is called when a disconnect message is received.
"""
# logger.warning("Received disconnect message: %s", message)
async def on_limit(self, track):
"""This method is a coroutine.
This is called when a limit notice is received.
"""
# logger.debug("Received limit notice: %d", track)
async def on_scrub_geo(self, notice):
"""This method is a coroutine.
This is called when a location deletion notice is received.
"""
# logger.debug("Received location deletion notice: %s", notice)
async def on_status_withheld(self, notice):
"""This method is a coroutine.
This is called when a status withheld content notice is received.
"""
# logger.debug("Received status withheld content notice: %s", notice)
async def on_user_withheld(self, notice):
"""This method is a coroutine.
This is called when a user withheld content notice is received.
"""
# logger.debug("Received user withheld content notice: %s", notice)
async def on_warning(self, notice):
"""This method is a coroutine.
This is called when a stall warning message is received.
"""
# logger.warning("Received stall warning: %s", notice)

444
main.py Normal file
View File

@ -0,0 +1,444 @@
#!/usr/local/bin/python3.9
import asyncio
from copy import copy
import datetime
import discord
from discord import errors
from discord.ext import commands
from discord.ext.commands import errors as ext_errors
from pathlib import Path
import sys
import traceback
import tweepy
from twitchAPI.twitch import Twitch
from kaizenbot import glob, logger
from kaizenbot.database import Database
from kaizenbot.user import User
from kaizenbot.utils import AsyncIntervalTimer, AsyncStream, IntervalTimer, Embeds
import kaizenbot.commands as cogs
class Bot(commands.Bot):
def __init__(self):
intents = discord.Intents(messages=True, members=True, guilds=True, reactions=True)
super().__init__(command_prefix='$', case_insensitive=True, intents=intents)
self.support = Support(self)
# ADDED AFTERWARDS: hardcoded password, very secure...
self.database = Database('kaizen', '9y*"xF(BxLZ!HpgKn_')
self.image_id = self.database.get_image_id()
self.message_id = self.database.get_message_id()
self.all_users = self.database.get_all_users()
self.normal_users = []
self.error_users = []
for cog in [cogs.Economy, cogs.Mod, cogs.Help, cogs.Info, cogs.Kaizen, cogs.Vote, cogs.NSFW]:
loaded_cog = cog(self)
if cog == cogs.Economy:
now = datetime.datetime.now()
try:
reset_economy = IntervalTimer((datetime.datetime(year=now.year, month=now.month, day=now.day + 1)), 60*60*24, lambda: loaded_cog.reset())
except ValueError:
reset_economy = IntervalTimer((datetime.datetime(year=now.year, month=now.month + 1, day=1)), 60 * 60 * 24, lambda: loaded_cog.reset())
reset_economy.start()
self.add_cog(loaded_cog)
@commands.Cog.listener()
async def on_ready(self):
logger.info('Logged in')
if not hasattr(self, 'guild'):
if sys.argv[1] == 'test':
self.guild: discord.Guild = self.get_guild(768846874517438535)
self.welcome_channel = self.guild.get_channel(769292296985903184)
self.info_channel = self.welcome_channel
self.twitter_channel = self.welcome_channel
self.kaizenianer = self.guild.get_role(802222058116612136)
self.twitter_abonnenten = self.kaizenianer
self.mod = self.guild.get_role(803296051444056115)
twitter_id = 1301397827378176007
elif sys.argv[1] == 'run':
self.guild: discord.Guild = self.get_guild(796132539269382154)
self.welcome_channel = self.guild.get_channel(796876279537336423)
self.info_channel = self.guild.get_channel(803403162056523786)
self.twitter_channel = self.guild.get_channel(797555712943194163)
self.kaizenianer = self.guild.get_role(796870178939994132)
self.twitter_abonnenten = self.guild.get_role(831963234352234578)
self.mod = self.guild.get_role(796864065741258773)
twitter_id = 982950038610632705
else:
exit(1)
return
now = datetime.datetime.now()
if now.hour < 12:
first_call = datetime.datetime(year=now.year, month=now.month, day=now.day, hour=12)
else:
try:
first_call = datetime.datetime(year=now.year, month=now.month, day=now.day + 1, hour=12)
except ValueError:
first_call = datetime.datetime(year=now.year, month=now.month + 1, day=1, hour=12)
glob['bot'] = self
daily_kaizen_infos = AsyncIntervalTimer((first_call - now).seconds, 60*60*24, Embeds.send_kaizen_infos, [self.info_channel])
glob['timers'].append(daily_kaizen_infos)
recheck_users = AsyncIntervalTimer(60*60*24, 60*60*24, self.support.recheck_user_tags_and_names)
glob['timers'].append(recheck_users)
database_ping = IntervalTimer(0, 60 * 60, lambda: self.database.cursor.execute('SELECT * FROM stuff WHERE k=?', ('0',)))
database_ping.start()
glob['timers'].append(database_ping)
logger.debug('Started database pinger')
transcript_cleaner = IntervalTimer(0, 60 * 5, self.support.transcript_cleaner)
transcript_cleaner.start()
glob['timers'].append(transcript_cleaner)
logger.debug('Started transcript cleaner')
twitch_stream_status = AsyncIntervalTimer(0, 60 * 5, self.support.twitch_stream_status)
glob['timers'].append(twitch_stream_status)
logger.debug("Started twitch stream status listener")
# ADDED AFTERWARDS: 2 twitter listeners, why not
# twitter listener
stream = AsyncStream('...', '...',
'...', '...')
stream.on_status = self.support.twitter(twitter_id)
await stream.filter(follow=[str(twitter_id)])
auth = tweepy.OAuthHandler('...', '...')
auth.set_access_token('...', '...')
glob['twitter'] = tweepy.API(auth)
logger.debug('Started twitter listener')
await self.support.on_startup()
self.loop = asyncio.get_event_loop()
self.loop.run_until_complete(await asyncio.gather([await self.support.check_normal_users()], return_exceptions=True))
@commands.Cog.listener()
async def help_command(self):
pass
@commands.Cog.listener()
async def on_guild_join(self, guild: discord.Guild):
if guild.id != self.guild.id:
await guild.leave()
@commands.Cog.listener()
async def on_connect(self):
logger.info('Connected to discord')
# self.loop = asyncio.get_event_loop()
# self.loop.run_until_complete(await self.support.check_normal_users())
@commands.Cog.listener()
async def on_disconnect(self):
logger.info('Disconnected from discord')
# self.loop.close()
@commands.Cog.listener()
async def on_command_error(self, ctx: commands.Context, error: ext_errors.CommandError):
if isinstance(error, ext_errors.CommandNotFound):
logger.info(error)
#elif isinstance(error, ext_errors.UnexpectedQuoteError):
# await ctx.send(embed=Embeds.error_embed('Please use `\'` as quotation mark for flags'))
elif isinstance(error, ext_errors.CommandOnCooldown):
cooldown = divmod(error.retry_after, 60)
await ctx.send(embed=Embeds.error_embed(description=f'Dieser Befehl kann erst wieder in {round(cooldown[0])} Minuten und {round(cooldown[1])} Sekunden genutzt werden'))
elif isinstance(error, ext_errors.MissingRequiredArgument):
await cogs.Help(self).show_help(ctx, ctx.command)
elif isinstance(error, ext_errors.MissingRole):
logger.info(f'{str(ctx.author)} tried to run `{ctx.command}` for which he has no authorization')
else:
logger.error(''.join(traceback.format_exception(etype=type(error), value=error, tb=error.__traceback__)))
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
if not payload.member.bot and payload.message_id == self.message_id:
user = self.all_users[payload.member.id]
if str(payload.emoji) == '':
if not user.accepted_rules_date:
await self.support.user_accepted_rules(user)
await payload.member.add_roles(self.kaizenianer)
else:
await payload.member.remove_roles(self.kaizenianer)
# `self.support.reset_user(user)` is not called here, because it gets called in `self.on_member_update(...)`
message = await self.welcome_channel.fetch_message(payload.message_id)
await message.remove_reaction(payload.emoji, payload.member)
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member):
if not member.bot:
join_message = await self.welcome_channel.send(f'Hey <@{member.id}>, Willkommen auf dem Offiziellen Discord-Server von Kaizen! **Kaizen Anime!**\n'
'Bitte lese die Regeln und warte 10 Mintuten, damit Du auf sie reacten kannst, um ein Kaizenianer zu werden! Diese findest Du, wenn Du nach oben scrollst, '
'oder indem Du zur angepinnten Nachricht springst.')
user = self.database.add_user(member, join_message.id)
self.all_users[user.id] = user
self.normal_users.append(user)
logger.info(f'User {str(member)} joined')
await asyncio.sleep(60*10 + 15)
if user.accepted_rules_date is None:
await member.send(f'Hey <@{member.id}>, Willkommen auf dem Offiziellen Discord-Server von Kaizen! **Kaizen Anime!**\n'
'Bitte lese und reacte auf die Regeln, um ein Kaizenianer zu werden! Diese findest Du, wenn Du im __**#willkommen**__ Kanal nach oben scrollst, oder indem Du zur dort angepinnten Nachricht springst.')
@commands.Cog.listener()
async def on_member_remove(self, member: discord.Member):
if not member.bot:
user = self.all_users[member.id]
self.database.remove_user(user.id)
await self.support.check_and_remove_normal_user(user)
del user
logger.info(f'User {str(member)} left')
@commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
try:
user = self.all_users[before.id]
except KeyError: # this method also gets called if a new user is joined
return
if hash((str(before), before.display_name)) != hash((str(after), after.display_name)):
self.database.change_user_infos(user, after.display_name, str(after))
logger.info(f'User changed from {str(before)}, {before.display_name} to {str(after)}, {after.display_name}')
if before.roles != after.roles:
if user.accepted_rules_date and self.kaizenianer in before.roles and self.kaizenianer not in after.roles:
self.support.reset_user(user)
elif not user.accepted_rules_date and self.kaizenianer not in before.roles and self.kaizenianer in after.roles:
await self.support.user_accepted_rules(user)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
glob['transcript'][datetime.datetime.now()] = f'[{datetime.datetime.now().strftime("%Y-%d-%m %H:%M:%S")}] - {message.author}: {message.content}' \
f'{" | Attachments: " + ", ".join([attachments.url for attachments in message.attachments]) if message.attachments else ""}'
await super().on_message(message)
class Support:
def __init__(self, bot: Bot):
self.bot = bot
self.streams = False
self.title = ""
self.twitch = Twitch('...', '...')
self.twitch.authenticate_app([])
async def on_startup(self):
# the rules embed to accept to get the kaizenianer role
embed = discord.Embed(description='Hey, bitte lese & akzeptiere unsere Regeln!\n\n'
'Willkommen auf dem Offiziellen Discord-Server von KaizenAnime. Bitte lest Euch die Regen einmal durch.\n\n'
'-Keine Beleidigungen\n'
'-Kein Rassismus\n'
'-Freundlich bleiben\n'
'-Themen bitte nur in die dafür vorgesehenen Kanäle posten\n'
'-Sprachkanäle bitte mit Respekt betreten\n'
'-Kein Spam oder Eigenwerbung\n'
'-Religiöse und Politische Themen sind nur in Sprachkanälen erlaubt\n'
'-Behandle alle mit Respekt. Belästigung, Hexenjagd, Sexismus, Rassismus oder Volksverhetzung werden absolut NICHT toleriert\n'
'\n'
'Die Regeln können jederzeit (mit Vorankündigung) geändert werden',
color=000000)
if not self.bot.image_id:
image = await self.bot.welcome_channel.send(file=discord.File(Path.cwd().joinpath('assets', 'rules.png')))
self.bot.database.set_image_id(image.id)
self.bot.image_id = image.id
if self.bot.message_id:
message = await self.bot.welcome_channel.fetch_message(self.bot.message_id)
await message.delete()
self.bot.database.set_message_id(None)
self.bot.message_id = None
# checks if the posted message if there are differences between `embed` content and the posted content
if self.bot.message_id:
message = await self.bot.welcome_channel.fetch_message(self.bot.message_id)
message_embed = message.embeds[0]
# if differences are there, the posted message will be edited
if hash((embed.title, embed.description, embed.color)) != hash((message_embed.title, message_embed.description, message_embed.color)):
await message.edit(embed=embed)
logger.info('Edited rules message')
# if the message doesn't exists, it gets posted
else:
message = await self.bot.welcome_channel.send(embed=embed)
await message.add_reaction('')
self.bot.database.set_message_id(message.id)
self.bot.message_id = message.id
member_ids = []
for member in self.bot.guild.members:
member_ids.append(member.id)
if not member.bot:
new = False
if member.id in self.bot.all_users:
user = self.bot.all_users[member.id]
else:
user = self.bot.database.add_user(member, None)
self.bot.all_users[member.id] = user
new = True
logger.info(f'User {user.tag} joined while I was offline')
if user.accepted_rules_date is None:
if self.bot.kaizenianer in member.roles:
self.bot.database.add_user_accepted_rules(user, datetime.datetime.now())
await self.check_and_remove_normal_user(user)
logger.info(f'Added kaizenianer {user.tag}')
else:
self.bot.normal_users.append(user)
if new:
join_message = await self.bot.welcome_channel.send(f'Hey <@{member.id}>, Willkommen auf dem Offiziellen Discord-Server von Kaizen! **Kaizen Anime!**\n'
f'Bitte lese und reacte auf die Regeln, um ein Kaizenianer zu werden! '
f'Diese findest Du, wenn Du nach oben scrollst, oder indem Du zur angepinnten Nachricht springst.')
await member.send(f'Hey <@{member.id}>, Willkommen auf dem Offiziellen Discord-Server von Kaizen! **Kaizen Anime!**\n'
f'Bitte lese und reacte auf die Regeln, um ein Kaizenianer zu werden! '
f'Diese findest Du, wenn Du im __**#willkommen**__ Kanal nach oben scrollst, oder indem Du zur dort angepinnten Nachricht springst.')
user.join_message = join_message.id
elif self.bot.kaizenianer not in member.roles:
self.bot.database.reset_user(user)
for user in copy(self.bot.all_users).values():
if user.id not in member_ids:
tag = user.tag
self.bot.database.remove_user(user.id)
del user
logger.info(f'User {tag} left while I was offline')
try:
reacted_users = message.reactions[0].users() # the ✅ reaction / emoji
async for reacted_user in reacted_users:
if not reacted_user.bot:
user = self.bot.all_users[reacted_user.id]
if not user.accepted_rules_date:
await reacted_user.add_roles(self.bot.kaizenianer)
await self.user_accepted_rules(user)
else:
self.reset_user(user)
await message.remove_reaction(message.reactions[0], user)
except IndexError: # gets thrown if the embed message was new created with this bot runtime
pass
bot.database.sync()
def transcript_cleaner(self):
now = datetime.datetime.now()
for timestamp in copy(glob['transcript']).keys():
if timestamp + datetime.timedelta(hours=1) < now:
del glob['transcript'][timestamp]
async def recheck_user_tags_and_names(self):
for member in self.bot.guild.members:
user = self.bot.all_users[member.id]
tag = str(member)
name = member.display_name
if tag != user.tag or name != user.name:
info = f'User changed from {user.tag}, {user.name} to {tag}, {name}'
self.bot.database.change_user_infos(user, name, tag)
logger.info(info)
async def check_normal_users(self):
temp_error_users = {}
while True:
now = datetime.datetime.now()
for user in self.bot.normal_users:
try:
if user.joined + datetime.timedelta(days=1) < now and user.warning_time is None:
member = self.bot.guild.get_member(user.id)
await member.send('Hey, bitte reacte auf unsere Server-Regeln, da dir sonst wegen Verweigerung ein Kick aus unserem Server (**Kaizen Anime**) bevorstehen wird!\n\n'
'Mit freundlichen Grüßen,\n'
'das _Kaizen Server Team_')
self.bot.database.set_user_warning(user, now)
logger.info(f'Warned user {user.tag}, because he didn\'t accepted the rules since he joined')
elif user.warning_time and user.warning_time + datetime.timedelta(days=1, hours=12) < now:
await self.bot.guild.get_member(user.id).kick(reason='Wegen nicht reacten auf unsere Kaizen Anime Server-Regeln trotz Verwarnung.\n\n'
'MfG das Kaizen Server Team')
# the user won't removed from the database here, because `self.on_member_remove(...)` is called
logger.info(f'Kicked user {user.tag} because he didn\'t accepted the rules')
except Exception as error:
if isinstance(error, errors.Forbidden):
if user not in self.bot.error_users:
if user in temp_error_users:
temp_error_users[user] += 1
else:
temp_error_users[user] = 1
if temp_error_users[user] >= 5:
logger.warning(f'User {user.tag} caused to many error. I hide error log messages for him from now on')
self.bot.error_users.append(user)
del temp_error_users[user]
continue
logger.warning(f'Unexpected exception was thrown while checking all normal users: {"".join(traceback.format_exception(etype=type(error), value=error, tb=error.__traceback__))}')
await asyncio.sleep(60)
async def check_and_remove_normal_user(self, user: User):
if user in self.bot.normal_users:
self.bot.normal_users.remove(user)
if user in self.bot.error_users:
self.bot.error_users.remove(user)
if user.join_message:
try:
message = await self.bot.welcome_channel.fetch_message(user.join_message)
await message.delete()
except discord.errors.NotFound:
logger.info(f'Failed to find message {user.join_message}')
user.join_message = None
def reset_user(self, user: User):
self.bot.database.reset_user(user)
self.bot.normal_users.append(user)
if user in self.bot.error_users:
self.bot.error_users.remove(user)
logger.info(f'Reset kaizenianer {user.tag}')
async def user_accepted_rules(self, user: User):
await self.check_and_remove_normal_user(user)
self.bot.database.add_user_accepted_rules(user, datetime.datetime.now())
if user in self.bot.error_users:
self.bot.error_users.remove(user)
logger.info(f'Added kaizenianer {user.tag}')
def twitter(self, id: int):
async def on_status(status: tweepy.Status):
# checks if the status author is the given id, if the tweet is not a retweet and if the tweet is not a reply
if id == status.author.id and not hasattr(status, 'retweeted_status') and status.in_reply_to_status_id is None:
msg = f'Hey <@&{self.bot.kaizenianer.id}>, **Kaizen!** hat einen neuen Tweet gepostet!\nhttps://twitter.com/twitter/statuses/{status.id}'
await self.bot.twitter_channel.send(msg)
return on_status
async def twitch_stream_status(self):
if data := self.twitch.get_streams(user_login=['kaizenanime'])['data']:
if not self.streams:
await self.bot.change_presence(activity=discord.Streaming(name=data[0]['title'], url='https://www.twitch.tv/kaizenanime'))
self.streams = True
logger.info('Kaizen started streaming')
elif self.title != data[0]['title']:
self.title = data[0]['title']
await self.bot.change_presence(activity=discord.Streaming(name=data[0]['title'], url='https://www.twitch.tv/kaizenanime'))
else:
if self.streams:
await self.bot.change_presence(activity=None)
self.streams = False
self.title = ''
logger.info('Kaizen stopped streaming')
if __name__ == '__main__':
bot = Bot()
if sys.argv[1] == 'test':
bot.run('...')
elif sys.argv[1] == 'run':
bot.run('...')