#!/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('...')