def run_migrations_online(): """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ database_uri = Config.get("database", "uri") if database_uri.startswith("sqlite"): connectable = create_engine( database_uri, connect_args={"check_same_thread": False}, poolclass=pool.NullPool, ) else: connectable = create_engine(database_uri, poolclass=pool.NullPool) with connectable.connect() as connection: context.configure( connection=connection, target_metadata=target_metadata, render_as_batch=database_uri.startswith("sqlite"), ) with context.begin_transaction(): context.run_migrations()
def __init__(self, session: Session, connect: bool = True, is_cycle: bool = False) -> None: self.users: Dict[str, ts3bot.User] = {} self.session = session self.is_cycle = is_cycle if is_cycle: self.client_nick = Config.get("cycle_login", "nickname") self.channel_id = None else: # Register commands self.commands: List[Command] = [] for _ in commands.__commands__: if Config.has_option("commands", _) and not Config.getboolean( "commands", _): LOG.info("Skipping command.%s", _) continue mod = cast(Command, import_module(f"ts3bot.commands.{_}")) mod.REGEX = re.compile(mod.MESSAGE_REGEX) LOG.info("Registered command.%s", _) self.commands.append(mod) # Register translation settings i18n.set("load_path", [str(Path(__file__).parent / "i18n")]) i18n.set("filename_format", "{locale}.json") i18n.set("enable_memoization", True) i18n.set("skip_locale_root_data", True) i18n.set("locale", "de") self.client_nick = Config.get("bot_login", "nickname") self.channel_id = Config.get("teamspeak", "channel_id") self.ts3c: Optional[ts3.query.TS3ServerConnection] = None self.own_id: int = 0 self.own_uid: str = "" if connect: self.connect()
def init_logger(name: str, is_test: bool = False) -> None: if not Path("logs").exists(): Path("logs").mkdir() logger = logging.getLogger() if os.environ.get("ENV", "") == "dev": level = logging.DEBUG else: level = logging.INFO logger.setLevel(level) fmt = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S") # Only write to file outside of tests if not is_test: hldr = logging.handlers.TimedRotatingFileHandler(f"logs/{name}.log", when="W0", encoding="utf-8", backupCount=16) hldr.setFormatter(fmt) logger.addHandler(hldr) stream = logging.StreamHandler(sys.stdout) stream.setFormatter(fmt) stream.setLevel(level) logger.addHandler(stream) sentry_dsn = Config.get("sentry", "dsn") if sentry_dsn: import sentry_sdk # type: ignore from sentry_sdk.integrations.sqlalchemy import ( SqlalchemyIntegration, ) # type: ignore def before_send(event: Any, hint: Any) -> Any: if "exc_info" in hint: _, exc_value, _ = hint["exc_info"] if isinstance(exc_value, KeyboardInterrupt): return None return event sentry_sdk.init( dsn=sentry_dsn, before_send=before_send, release=VERSION, send_default_pii=True, integrations=[SqlalchemyIntegration()], )
def connect(self) -> None: if self.is_cycle: username = Config.get("cycle_login", "username") password = Config.get("cycle_login", "password") else: username = Config.get("bot_login", "username") password = Config.get("bot_login", "password") # Connect to TS3 self.ts3c = ts3.query.TS3ServerConnection( f"{Config.get('teamspeak', 'protocol')}://{username}:{password}@" f"{Config.get('teamspeak', 'hostname')}") # Select server and change nick self.exec_("use", sid=Config.get("teamspeak", "server_id")) current_nick = self.exec_("whoami") if current_nick[0]["client_nickname"] != self.client_nick: self.exec_("clientupdate", client_nickname=self.client_nick) # TODO: Replace clientfind/clientinfo with info from whoami self.own_id = self.exec_("clientfind", pattern=self.client_nick)[0]["clid"] self.own_uid = self.exec_( "clientinfo", clid=self.own_id)[0]["client_unique_identifier"] if not self.is_cycle: # Subscribe to events self.exec_("servernotifyregister", event="channel", id=self.channel_id) self.exec_("servernotifyregister", event="textprivate") self.exec_("servernotifyregister", event="server") # Move to target channel if current_nick[0]["client_channel_id"] != self.channel_id: self.exec_("clientmove", clid=self.own_id, cid=self.channel_id)
def run_migrations_offline(): """Run migrations in 'offline' mode. This configures the context with just a URL and not an Engine, though an Engine is acceptable here as well. By skipping the Engine creation we don't even need a DBAPI to be available. Calls to context.execute() here emit the given string to the script output. """ url = Config.get("database", "uri") context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) with context.begin_transaction(): context.run_migrations()
def connect(self): if self.is_cycle: username = Config.get("cycle_login", "username") password = Config.get("cycle_login", "password") else: username = Config.get("bot_login", "username") password = Config.get("bot_login", "password") # Connect to TS3 self.ts3c = ts3.query.TS3ServerConnection( "{}://{}:{}@{}".format( Config.get("teamspeak", "protocol"), username, password, Config.get("teamspeak", "hostname"), ) ) # Select server and change nick self.exec_("use", sid=Config.get("teamspeak", "server_id")) current_nick = self.exec_("whoami") if current_nick[0]["client_nickname"] != self.client_nick: self.exec_("clientupdate", client_nickname=self.client_nick) self.own_id: int = self.exec_("clientfind", pattern=self.client_nick)[0]["clid"] self.own_uid = self.exec_("clientinfo", clid=self.own_id)[0][ "client_unique_identifier" ] if not self.is_cycle: # Subscribe to events self.exec_("servernotifyregister", event="channel", id=self.channel_id) self.exec_("servernotifyregister", event="textprivate") self.exec_("servernotifyregister", event="server") # Move to target channel if current_nick[0]["client_channel_id"] != self.channel_id: self.exec_("clientmove", clid=self.own_id, cid=self.channel_id)
def verify_user( self, client_unique_id: str, client_database_id: str, client_id: str ) -> bool: """ Verify a user if they are in a known group, otherwise nothing is done. Groups are revoked/updated if necessary :param client_unique_id: The client's UUID :param client_database_id: The database ID :param client_id: The client's temporary ID during the session :return: True if the user has/had a known group and False if the user is new """ def revoked(response: str): if account: account.invalidate(self.session) changes = ts3bot.sync_groups( self, client_database_id, account, remove_all=True ) reason = "unknown reason" if response == "groups_revoked_missing_key": reason = "missing API key" elif response == "groups_revoked_invalid_key": reason = "invalid API key" logging.info( "Revoked user's (cldbid:%s) groups (%s) due to %s.", client_database_id, changes["removed"], reason, ) self.send_message(client_id, response) # Get all current groups server_groups = self.exec_("servergroupsbyclientid", cldbid=client_database_id) known_groups: typing.List[int] = ( [ _.group_id for _ in self.session.query(ts3bot.database.models.WorldGroup).options( load_only(ts3bot.database.models.WorldGroup.group_id) ) ] + [ _.group_id for _ in self.session.query(ts3bot.database.models.Guild) .filter(ts3bot.database.models.Guild.group_id.isnot(None)) .options(load_only(ts3bot.database.models.Guild.group_id)) ] + [ int(Config.get("teamspeak", "generic_world_id")), int(Config.get("teamspeak", "generic_guild_id")), ] ) # Check if user has any known groups has_group = False has_skip_group = False for server_group in server_groups: if int(server_group.get("sgid", -1)) in known_groups: has_group = True if server_group.get("name") in Config.whitelist_groups: has_skip_group = True # Skip users without any known groups if not has_group: return False # Skip users in whitelisted groups that should be ignored like # guests, music bots, etc if has_skip_group: return True # Grab user's account info account = models.Account.get_by_identity(self.session, client_unique_id) # User does not exist in DB if not account: revoked("groups_revoked_missing_key") return True # User was checked, don't check again if ts3bot.timedelta_hours( datetime.datetime.today() - account.last_check ) < Config.getfloat("verify", "on_join_hours"): return True logging.debug("Checking %s/%s", account, client_unique_id) try: account.update(self.session) # Sync groups ts3bot.sync_groups(self, client_database_id, account) except ts3bot.InvalidKeyException: revoked("groups_revoked_invalid_key") except ( requests.RequestException, ts3bot.RateLimitException, ts3bot.ApiErrBadData, ): logging.exception("Error during API call") return True
def handle_event(self, event: ts3.response.TS3Event): evt = events.Event.from_event(event) # Got an event where the DB is relevant if event.event in ["notifycliententerview", "notifytextmessage"]: try: # Skip check when using SQLite if self.session.bind.name != "sqlite": self.session.execute("SELECT VERSION()") except exc.DBAPIError as e: if e.connection_invalidated: logging.debug("Database connection was invalidated") else: raise finally: # Close session anyway to force-use a new connection self.session.close() if isinstance(evt, events.ClientEnterView): # User connected/entered view # Skip server query and other non-voice clients if evt.client_type != "0": return if not evt.id: return was_created = self.create_user(evt.id) is_known = self.verify_user(evt.uid, evt.database_id, evt.id) # Skip next check if user could not be cached if not was_created: return # Message user if total_connections is below n and user is new annoy_limit = Config.getint("teamspeak", "annoy_total_connections") if ( not is_known and evt.id in self.users and -1 < self.users[evt.id].total_connections <= annoy_limit ): self.send_message(evt.id, "welcome_greet", con_limit=annoy_limit) elif isinstance(evt, events.ClientLeftView): if evt.id and evt.id in self.users: del self.users[evt.id] elif isinstance(evt, events.ClientMoved): if evt.channel_id == str(self.channel_id): logging.info("User id:%s joined channel", evt.id) self.send_message(evt.id, "welcome") else: logging.info("User id:%s left channel", evt.id) elif isinstance(evt, events.TextMessage): if not evt.id: invoker_id = event[0].get("invokerid") if invoker_id: self.send_message(invoker_id, "parsing_error") return logging.info("Message from %s (%s): %s", evt.name, evt.uid, evt.message) valid_command = False for command in self.commands: match = command.REGEX.match(evt.message) if match: valid_command = True try: command.handle(self, evt, match) except ts3.query.TS3QueryError: logging.exception( "Unexpected TS3QueryError in command handler." ) break if not valid_command: self.send_message(evt.id, "invalid_input") else: logging.warning("Unexpected event: %s", event.data)
from logging.config import fileConfig from alembic import context # type: ignore from sqlalchemy import create_engine, pool # Force module path sys.path.insert( 0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))) from ts3bot.config import Config from ts3bot.database.models.base import Base # Load custom config Config.load() # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) # type: ignore # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata target_metadata = Base.metadata
def sync_groups( bot: ts3_bot.Bot, cldbid: str, account: Optional[models.Account], remove_all: bool = False, skip_whitelisted: bool = False, ) -> SyncGroupChanges: def _add_group(group: ServerGroup) -> bool: """ Adds a user to a group if necessary, updates `server_group_ids`. :param group: :return: """ if int(group["sgid"]) in server_group_ids: return False try: bot.exec_("servergroupaddclient", sgid=str(group["sgid"]), cldbid=cldbid) LOG.info("Added user dbid:%s to group %s", cldbid, group["name"]) server_group_ids.append(int(group["sgid"])) group_changes["added"].append(group["name"]) except ts3.TS3Error: # User most likely doesn't have the group LOG.exception( "Failed to add cldbid:%s to group %s for some reason.", cldbid, group["name"], ) return True def _remove_group(group: ServerGroup) -> bool: """ Removes a user from a group if necessary, updates `server_group_ids`. :param group: :return: """ if int(group["sgid"]) in server_group_ids: try: bot.exec_("servergroupdelclient", sgid=str(group["sgid"]), cldbid=cldbid) LOG.info("Removed user dbid:%s from group %s", cldbid, group["name"]) server_group_ids.remove(int(group["sgid"])) group_changes["removed"].append(group["name"]) return True except ts3.TS3Error: # User most likely doesn't have the group LOG.exception( "Failed to remove cldbid:%s from group %s for some reason.", cldbid, group["name"], ) return False server_groups = bot.exec_("servergroupsbyclientid", cldbid=cldbid) server_group_ids = [int(_["sgid"]) for _ in server_groups] group_changes: SyncGroupChanges = {"removed": [], "added": []} # Get groups the user is allowed to have if account and account.is_valid and not remove_all: valid_guild_groups: List[ models.LinkAccountGuild] = account.guild_groups() is_part_of_alliance = account.is_in_alliance() else: valid_guild_groups = [] is_part_of_alliance = False valid_guild_group_ids = cast( List[int], [g.guild.group_id for g in valid_guild_groups]) valid_guild_mapper = {g.guild.group_id: g for g in valid_guild_groups} # Get all valid groups guild_groups: List[int] = [ _.group_id for _ in bot.session.query(models.Guild).filter( models.Guild.group_id.isnot(None)).options( load_only(models.Guild.group_id)) ] generic_alliance_group = ServerGroup( sgid=int(Config.get("teamspeak", "generic_alliance_id")), name="Generic Alliance Group", ) generic_guild = ServerGroup(sgid=int( Config.get("teamspeak", "generic_guild_id")), name="Generic Guild") # Remove user from all other known invalid groups invalid_groups = [] for server_group in server_groups: sgid = int(server_group["sgid"]) # Skip known valid groups if (server_group["name"] == "Guest" or sgid == generic_alliance_group or sgid == generic_guild or (len(valid_guild_groups) > 0 and sgid in valid_guild_group_ids) or sgid == generic_alliance_group["sgid"]): continue # Skip users with whitelisted group if skip_whitelisted and server_group.get( "name") in Config.whitelist_groups: LOG.info( "Skipping cldbid:%s due to whitelisted group: %s", cldbid, server_group.get("name"), ) return group_changes # Skip unknown groups if sgid not in guild_groups: continue invalid_groups.append(server_group) for server_group in invalid_groups: _remove_group(server_group) # User has additional guild groups but shouldn't if len(valid_guild_group_ids) == 0: for _group in Config.additional_guild_groups: for server_group in server_groups: if server_group["name"] == _group: _remove_group(server_group) break # User is missing generic guild if len(valid_guild_group_ids ) > 0 and generic_guild["sgid"] not in server_group_ids: _add_group(generic_guild) # User has generic guild but shouldn't if len(valid_guild_group_ids ) == 0 and generic_guild["sgid"] in server_group_ids: _remove_group(generic_guild) # Sync guild groups left_guilds = [ gid for gid in server_group_ids if gid in guild_groups and gid not in valid_guild_group_ids ] # Guilds that shouldn't be applied to the user joined_guilds = [ gid for gid in valid_guild_group_ids if gid not in server_group_ids ] # Guild that are missing in the user's group list # Remove guilds that should not be applied if len(guild_groups) > 0: for group_id in left_guilds: _remove_group( ServerGroup(sgid=group_id, name=valid_guild_mapper[group_id].guild.name)) # Join guilds if len(valid_guild_group_ids) > 0: for group_id in joined_guilds: _add_group( ServerGroup(sgid=group_id, name=valid_guild_mapper[group_id].guild.name)) # User is missing alliance group if is_part_of_alliance and generic_alliance_group[ "sgid"] not in server_group_ids: _add_group(generic_alliance_group) # User has alliance group but shouldn't if not is_part_of_alliance and generic_alliance_group[ "sgid"] in server_group_ids: _remove_group(generic_alliance_group) return group_changes