Initial commit

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

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)