Exemple #1
0
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()
Exemple #2
0
    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()
Exemple #3
0
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()],
        )
Exemple #4
0
    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)
Exemple #5
0
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()
Exemple #6
0
    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)
Exemple #7
0
    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
Exemple #8
0
    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)
Exemple #9
0
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
Exemple #10
0
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