445 lines
23 KiB
Python
445 lines
23 KiB
Python
|
#!/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('...')
|