Beispiel #1
0
def handle(bot: Bot, event: events.TextMessage, _match: typing.Match):
    if event.uid not in Config.whitelist_admin:
        return

    i18n.set("locale", "en")
    message = i18n.t("available_commands")
    for _ in bot.commands:
        if _.USAGE != USAGE:
            message += "\n - {}".format(_.USAGE)

    bot.send_message(event.id, message, is_translation=False)
Beispiel #2
0
    def __init__(
        self,
        session: Session,
        verify_all: bool,
        verify_ts3: bool,
    ):
        self.bot = Bot(session, is_cycle=True)
        self.session = session
        self.verify_all = verify_all
        self.verify_ts3 = verify_ts3

        self.verify_begin = datetime.datetime.today()
Beispiel #3
0
    def __init__(
        self,
        session: Session,
        verify_all: bool,
        verify_linked_worlds: bool,
        verify_ts3: bool,
        verify_world: typing.Optional[int] = None,
    ):
        self.bot = Bot(session, is_cycle=True)
        self.session = session
        self.verify_all = verify_all
        self.verify_linked_worlds = verify_linked_worlds
        self.verify_ts3 = verify_ts3

        if verify_world:
            self.verify_world: typing.Optional[enums.World] = enums.World(verify_world)
        else:
            self.verify_world = None

        self.verify_begin = datetime.datetime.today()
Beispiel #4
0
def handle(bot: Bot, event: events.TextMessage, match: typing.Match):
    try:
        account = fetch_api("account", api_key=match.group(1))
        server = enums.World(account.get("world"))

        guilds = (
            bot.session.query(models.Guild)
            .filter(models.Guild.guid.in_(account.get("guilds", [])))
            .filter(models.Guild.group_id.isnot(None))
            .options(load_only(models.Guild.name))
        )

        bot.send_message(
            event.id,
            "info_world",
            user=account.get("name"),
            world=server.proper_name,
            guilds=", ".join([_.name for _ in guilds]),
        )
    except InvalidKeyException:
        logging.info("This seems to be an invalid API key.")
        bot.send_message(event.id, "invalid_token")
    except (requests.RequestException, RateLimitException, ApiErrBadData):
        logging.exception("Error during API call")
        bot.send_message(event.id, "error_api")
Beispiel #5
0
def handle(bot: Bot, event: events.TextMessage, match: typing.Match):
    if event.uid not in Config.whitelist_admin:
        return

    # Grab client_uid
    try:
        user = bot.exec_("clientgetnamefromdbid", cldbid=match.group(1))
        client_uid = user[0]["cluid"]
    except ts3.query.TS3QueryError:
        bot.send_message(event.id, "user_not_found")
        return

    try:
        json = fetch_api("account", api_key=match.group(2))
        account = models.Account.get_or_create(bot.session, json,
                                               match.group(2))
        identity: models.Identity = models.Identity.get_or_create(
            bot.session, client_uid)

        # Save api key in account
        account.api_key = match.group(2)
        account.is_valid = True
        bot.session.commit()

        transfer_registration(
            bot,
            account,
            event,
            is_admin=True,
            target_identity=identity,
            target_dbid=match.group(1),
        )
    except InvalidKeyException:
        logging.info("This seems to be an invalid API key.")
        bot.send_message(event.id, "invalid_token")
        return
    except (RateLimitException, RequestException, ApiErrBadData):
        bot.send_message(event.id, "error_api")
Beispiel #6
0
def fetch_identity_guilds(session: Session):
    logging.info("--- Migrating user's current guild ---")

    valid_guilds = session.query(Guild).filter(
        Guild.group_id.isnot(None)).all()
    guild_mapper = {}
    for guild in valid_guilds:
        guild_mapper[guild.group_id] = guild

    # Create TS3 bot
    bot = Bot(session, is_cycle=True)

    num_identities = session.query(Identity).count()

    # Fetch identity's guild
    for idx, identity in enumerate(session.query(Identity).all()):
        logging.info("%s/%s: Checking %s", idx + 1, num_identities, identity)

        try:
            cldbid = bot.exec_("clientgetdbidfromuid",
                               cluid=identity.guid)[0]["cldbid"]
            server_groups = bot.exec_("servergroupsbyclientid", cldbid=cldbid)

            guild = None
            # Find known guild
            for server_group in server_groups:
                if int(server_group["sgid"]) in guild_mapper:
                    guild = guild_mapper[int(server_group["sgid"])]

            # User has no valid guild
            if guild is None:
                continue

            account = Account.get_by_identity(session, identity.guid)

            if not account:
                logging.warning(
                    "%s/%s: %s is not linked to any account",
                    idx + 1,
                    num_identities,
                    identity,
                )
                continue

            logging.info("%s/%s: Linking %s to %s", idx + 1, num_identities,
                         identity, guild)

            # Unlink all current
            session.query(LinkAccountGuild).filter(
                LinkAccountGuild.account_id == account.id).update(
                    {"is_active": False})

            # Link correct guild
            session.query(LinkAccountGuild).filter(
                and_(
                    LinkAccountGuild.guild_id == guild.id,
                    LinkAccountGuild.account_id == account.id,
                )).update({"is_active": True})

            session.commit()

        except ts3.TS3Error as e:
            if e.args[0].error["id"] == "512":
                logging.info("%s does not exist on server, deleting.",
                             identity)
                session.query(LinkAccountIdentity).filter(
                    LinkAccountIdentity.identity == identity).delete()
                session.delete(identity)
                session.commit()
            else:
                logging.info("Failed to link %s", identity, exc_info=True)

    logging.info("--- Migrating current guilds done ---")
Beispiel #7
0
def handle(bot: Bot, event: events.TextMessage, match: typing.Match):
    cldbid = bot.exec_("clientgetdbidfromuid", cluid=event.uid)[0]["cldbid"]

    # Grab user's account
    account = models.Account.get_by_identity(bot.session, event.uid)

    if not account or not account.is_valid:
        bot.send_message(event.id, "missing_token")
        return

    on_join_hours_timeout = Config.getfloat("verify", "on_join_hours")

    # Saved account is older than x hours or has no guilds
    if (
        timedelta_hours(datetime.datetime.today() - account.last_check)
        >= on_join_hours_timeout
        or account.guilds.count() == 0
    ):
        bot.send_message(event.id, "account_updating")

        try:
            account.update(bot.session)

            # Sync groups in case the user has left a guild or similar changes
            sync_groups(bot, cldbid, account)
        except InvalidKeyException:
            # Invalidate link
            account.invalidate(bot.session)
            sync_groups(bot, cldbid, account, remove_all=True)

            logging.info("Revoked user's permissions.")
            bot.send_message(event.id, "invalid_token_admin")
            return
        except (requests.RequestException, RateLimitException, ApiErrBadData):
            logging.exception("Error during API call")
            bot.send_message(event.id, "error_api")

    # User requested guild removal
    if match.group(1) and match.group(1).lower() == "remove":
        # Get active guilds
        has_active_guilds: int = (
            account.guilds.join(models.Guild)
            .filter(models.Guild.group_id.isnot(None))
            .filter(models.LinkAccountGuild.is_active.is_(True))
            .count()
        )

        # There are no active guilds, no need to remove anything
        if not has_active_guilds:
            bot.send_message(event.id, "guild_already_removed")
            return

        # Remove guilds
        account.guilds.filter(models.LinkAccountGuild.is_active.is_(True)).update(
            {"is_active": False}
        )
        bot.session.commit()

        # Sync groups
        changes = sync_groups(bot, cldbid, account)
        if len(changes["removed"]) > 0:
            bot.send_message(event.id, "guild_removed")
        else:
            bot.send_message(event.id, "guild_error")

        return

    available_guilds = account.guilds.join(models.Guild).filter(
        models.Guild.group_id.isnot(None)
    )

    # No guild specified
    if not match.group(1):
        available_guilds = available_guilds.all()
        if len(available_guilds) > 0:
            bot.send_message(
                event.id,
                "guild_selection",
                guilds="\n- ".join([_.guild.tag for _ in available_guilds]),
            )
        else:
            bot.send_message(event.id, "guild_unknown")
    else:
        guild = match.group(1).lower()

        selected_guild: typing.Optional[
            models.LinkAccountGuild
        ] = available_guilds.filter(models.Guild.tag.ilike(guild)).one_or_none()

        # Guild not found or user not in guild
        if not selected_guild:
            bot.send_message(
                event.id, "guild_invalid_selection", timeout=on_join_hours_timeout
            )
            return

        # Toggle guild
        if selected_guild.is_active:
            selected_guild.is_active = False
        else:
            selected_guild.is_active = True

            # Remove other guilds if only one is allowed
            if not Config.getboolean("guild", "allow_multiple_guilds"):
                account.guilds.filter(
                    models.LinkAccountGuild.id != selected_guild.id
                ).update({"is_active": False})

        bot.session.commit()

        # Sync groups
        changes = sync_groups(bot, cldbid, account)
        if selected_guild.is_active and len(changes["added"]):
            bot.send_message(event.id, "guild_set", guild=selected_guild.guild.name)
        elif not selected_guild.is_active and len(changes["removed"]):
            bot.send_message(
                event.id, "guild_removed_one", guild=selected_guild.guild.name
            )
        else:
            bot.send_message(event.id, "guild_error")
Beispiel #8
0
class Cycle:
    def __init__(
        self,
        session: Session,
        verify_all: bool,
        verify_linked_worlds: bool,
        verify_ts3: bool,
        verify_world: typing.Optional[int] = None,
    ):
        self.bot = Bot(session, is_cycle=True)
        self.session = session
        self.verify_all = verify_all
        self.verify_linked_worlds = verify_linked_worlds
        self.verify_ts3 = verify_ts3

        if verify_world:
            self.verify_world: typing.Optional[enums.World] = enums.World(verify_world)
        else:
            self.verify_world = None

        self.verify_begin = datetime.datetime.today()

    def revoke(self, account: typing.Optional[models.Account], cldbid: str):
        if account:
            account.invalidate(self.session)

        changes = ts3bot.sync_groups(
            self.bot, cldbid, account, remove_all=True, skip_whitelisted=True
        )
        if len(changes["removed"]) > 0:
            logging.info(
                "Revoked user's (cldbid:%s) groups (%s).", cldbid, changes["removed"]
            )
        else:
            logging.debug("Removed no groups from user (cldbid:%s).", cldbid)

    def fix_user_guilds(self):
        """
        Removes duplicate selected guilds from users.
        No need to force-sync the user as that's done on join and in the
        following verification function.
        """

        duplicate_guilds = (
            self.session.query(models.LinkAccountGuild)
            .filter(models.LinkAccountGuild.is_active.is_(True))
            .group_by(models.LinkAccountGuild.account_id)
            .having(func.count(models.LinkAccountGuild.guild_id) > 1)
        )
        for row in duplicate_guilds:
            logging.warning(f"{row.account} has multiple guilds.")

            # Delete duplicates
            self.session.query(models.LinkAccountGuild).filter(
                models.LinkAccountGuild.id
                != (
                    self.session.query(models.LinkAccountGuild)
                    .filter(models.LinkAccountGuild.account_id == row.account_id)
                    .filter(models.LinkAccountGuild.is_active.is_(True))
                    .order_by(models.LinkAccountGuild.id.desc())
                    .options(load_only(models.LinkAccountGuild.id))
                    .limit(1)
                    .subquery()
                )
            ).delete(synchronize_session="fetch")

    def run(self):
        # Skip check if multiple guilds are allowed
        if not Config.getboolean("guild", "allow_multiple_guilds"):
            self.fix_user_guilds()

        # Run if --ts3 is set or nothing was passed
        if self.verify_ts3 or not (
            self.verify_all or self.verify_linked_worlds or self.verify_world
        ):
            self.verify_ts3_accounts()

        self.verify_accounts()

        # Clean up "empty" guilds
        models.Guild.cleanup(self.session)

    def verify_ts3_accounts(self):
        # Retrieve users
        users = self.bot.exec_("clientdblist", duration=200)
        start = 0
        while len(users) > 0:
            for counter, user in enumerate(users):
                uid = user["client_unique_identifier"]
                cldbid = user["cldbid"]

                # Skip SQ account
                if "ServerQuery" in uid:
                    continue

                # Send keepalive
                if counter % 100 == 0:
                    self.bot.ts3c.send_keepalive()

                # Get user's account
                account = models.Account.get_by_identity(self.session, uid)

                if not account:
                    self.revoke(None, cldbid)
                else:
                    # User was checked, don't check again
                    if ts3bot.timedelta_hours(
                        datetime.datetime.today() - account.last_check
                    ) < Config.getfloat("verify", "cycle_hours") and not (
                        self.verify_all
                    ):
                        continue

                    logging.info("Checking %s/%s", account, uid)

                    try:
                        account.update(self.session)
                        # Sync groups
                        ts3bot.sync_groups(self.bot, cldbid, account)
                    except ts3bot.InvalidKeyException:
                        self.revoke(account, cldbid)
                    except ts3bot.ApiErrBadData:
                        logging.warning(
                            "Got ErrBadData for this account after multiple attempts."
                        )
                    except requests.RequestException:
                        logging.exception("Error during API call")
                        raise

            # Skip to next user block
            start += len(users)
            try:
                users = self.bot.exec_("clientdblist", start=start, duration=200)
            except ts3.query.TS3QueryError as e:
                # Fetching users failed, most likely at end
                if e.args[0].error["id"] != "1281":
                    logging.exception("Error retrieving user list")
                users = []

    def verify_accounts(self):
        """
        Removes users from known groups if no account is known or the account is invalid
        """
        # Update all other accounts
        if self.verify_all:
            # Check all accounts that were not verified just now
            accounts = self.session.query(models.Account).filter(
                and_(
                    models.Account.last_check <= self.verify_begin,
                    models.Account.is_valid.is_(True),
                )
            )
        elif self.verify_linked_worlds:
            # Check all accounts which are on linked worlds, or on --world
            def or_world():
                if self.verify_world:
                    return or_(
                        models.Account.world == self.verify_world,
                        models.WorldGroup.is_linked.is_(True),
                    )
                else:
                    return models.WorldGroup.is_linked.is_(True)

            accounts = (
                self.session.query(models.Account)
                .join(
                    models.WorldGroup,
                    models.Account.world == models.WorldGroup.world,
                    isouter=True,
                )
                .filter(
                    and_(
                        models.Account.last_check <= self.verify_begin,
                        or_world(),
                        models.Account.is_valid.is_(True),
                    )
                )
            )
        elif self.verify_world:
            # Only check accounts of this world
            accounts = self.session.query(models.Account).filter(
                and_(
                    models.Account.last_check
                    <= datetime.datetime.today()
                    - datetime.timedelta(
                        hours=Config.getfloat("verify", "cycle_hours")
                    ),
                    models.Account.is_valid.is_(True),
                    models.Account.world == self.verify_world,
                )
            )
        else:
            # Check all accounts which were not checked <x hours ago
            accounts = self.session.query(models.Account).filter(
                and_(
                    models.Account.last_check
                    <= datetime.datetime.today()
                    - datetime.timedelta(
                        hours=Config.getfloat("verify", "cycle_hours")
                    ),
                    models.Account.is_valid.is_(True),
                )
            )

        num_accounts = accounts.count()

        for idx, account in enumerate(accounts):
            if idx % 100 == 0 or idx - 1 == num_accounts:
                logging.info("%s/%s: Checking %s", idx + 1, num_accounts, account.name)

            try:
                account.update(self.session)
            except ts3bot.InvalidKeyException:
                pass
            except ts3bot.ApiErrBadData:
                logging.warning(
                    "Got ErrBadData for this account after multiple attempts, ignoring for now."
                )
            except requests.RequestException:
                logging.exception("Error during API call")
                raise
Beispiel #9
0
def handle(bot: Bot, event: events.TextMessage, match: Match) -> None:
    sheet_channel_id = Config.get("teamspeak", "sheet_channel_id")
    if sheet_channel_id == 0:
        return

    current_state: CommandingDict = {
        "EBG": [],
        "Red": [],
        "Green": [],
        "Blue": []
    }

    if match.group(1) == "help" and event.uid in Config.whitelist_admin:
        bot.send_message(
            event.id,
            "!sheet <ebg,red,green,blue,remove,reset>\n!sheet set <ebg,red,green,blue,remove> <name>",
            is_translation=False,
        )
        return

    if match.group(1) == "reset" and event.uid in Config.whitelist_admin:
        pass  # Don't load the current file, just use the defaults
    elif match.group(1) == "set" and event.uid in Config.whitelist_admin:
        # Force-set an entry
        _match = re.match(
            "!sheet set (ebg|red|green|blue|r|g|b|remove) (.*)",
            event.message.strip(),
        )
        if not _match:
            bot.send_message(event.id, "invalid_input")
            return

        if STATE_FILE.exists():
            current_state = cast(CommandingDict,
                                 json.loads(STATE_FILE.read_text()))

        if _match.group(1) == "remove":
            current_state = _remove_lead(current_state,
                                         name_field=_match.group(2))
        else:
            # Add new entry
            new_state = _add_lead(
                current_state,
                wvw_map=_match.group(1),
                note="",
                name=_match.group(2),
            )
            if not new_state:
                bot.send_message(event.id, "sheet_map_full")
                return
            current_state = new_state

    elif match.group(1) in [
            "ebg", "red", "green", "blue", "r", "g", "b", "remove"
    ]:
        if STATE_FILE.exists():
            current_state = json.loads(STATE_FILE.read_text())

        if match.group(1) == "remove":
            current_state = _remove_lead(current_state, uid=event.uid)
        else:
            new_state = _add_lead(
                current_state,
                wvw_map=match.group(1),
                note=match.group(2),
                uid=event.uid,
                name=event.name,
            )
            if not new_state:
                bot.send_message(event.id, "sheet_map_full")
                return
            current_state = new_state
    else:
        bot.send_message(event.id, "invalid_input")
        return

    # Build new table
    desc = "[table][tr][td] | Map | [/td][td] | Lead | [/td][td] | Note | [/td][td] | Date | [/td][/tr]"
    for _map, leads in cast(IterType, current_state.items()):
        if len(leads) == 0:
            desc += f"[tr][td]{_map}[/td][td]-[/td][td]-[/td][td]-[/td][/tr]"
            continue

        for lead in leads:
            desc += (
                f"[tr][td]{_map}[/td][td]{lead['lead']}[/td][td]{_encode(lead['note'])}[/td]"
                f"[td]{lead['date']}[/td][/tr]")

    desc += (
        f"[/table]\n[hr]Last change: {_tidy_date()}\n\n"
        f"Link to bot: [URL=client://0/{bot.own_uid}]{Config.get('bot_login', 'nickname')}[/URL]\n"  # Add link to self
        "Usage:\n"
        "- !sheet red/green/blue (note)\t—\tRegister your lead with an optional note (20 characters).\n"
        "- !sheet remove\t—\tRemove the lead")
    bot.exec_("channeledit", cid=sheet_channel_id, channel_description=desc)
    bot.send_message(event.id, "sheet_changed")

    STATE_FILE.write_text(json.dumps(current_state))
Beispiel #10
0
        help=
        "Only verifies everyone on a world marked as is_linked, ignores cycle_hours",
        action="store_true",
    )
    sub_cycle.add_argument(
        "--ts3",
        help="Verify everyone known to the TS3 server, this is the default",
        action="store_true",
    )
    sub_cycle.add_argument("--world", help="Verify world (id)", type=int)
    sub.add_parser("bot", help="Runs the main bot")

    args = parser.parse_args()

    if args.mode == "bot":
        init_logger("bot")
        Bot(create_session(Config.get("database", "uri"))).loop()
    elif args.mode == "cycle":
        init_logger("cycle")
        Cycle(
            create_session(Config.get("database", "uri")),
            verify_all=args.all,
            verify_ts3=args.ts3,
        ).run()
    else:
        parser.print_help()

# TODO: !help: Respond with appropriate commands
# TODO: Wrapper for servergroupaddclient/servergroupdelclient
# TODO: API timeout, async rewrite
Beispiel #11
0
def apply_generic_groups(session: Session):
    """
    Applies generic world/guild groups for users in known groups
    :param session:
    :return:
    """
    logging.info("--- Applying generic permission groups to users ---")
    logging.warning("Updating the permissions is a manual action!")

    guild_groups = {
        _.group_id: _.name
        for _ in session.query(Guild).filter(Guild.group_id.isnot(
            None)).options(load_only(Guild.group_id, Guild.name))
    }
    world_groups = {
        _.group_id: _.world
        for _ in session.query(WorldGroup).filter(
            WorldGroup.is_linked.is_(True)).options(
                load_only(WorldGroup.group_id, WorldGroup.world))
    }
    generic_world = int(Config.get("teamspeak", "generic_world_id"))
    generic_guild = int(Config.get("teamspeak", "generic_guild_id"))

    # Create TS3 bot
    bot = Bot(session, is_cycle=True)

    users = bot.exec_("clientdblist", duration=200)
    start = 0
    while len(users) > 0:
        for counter, user in enumerate(users):
            uid = user["client_unique_identifier"]
            cldbid = user["cldbid"]

            # Skip SQ account
            if "ServerQuery" in uid:
                continue

            # Send keepalive
            if counter % 100 == 0:
                bot.ts3c.send_keepalive()

            # Get user's groups
            server_groups = bot.exec_("servergroupsbyclientid", cldbid=cldbid)

            has_guild = None
            has_linked_world = None
            groups_exist = False
            for server_group in server_groups:
                sgid = int(server_group["sgid"])
                if sgid in world_groups:
                    has_linked_world = world_groups[sgid]
                elif sgid in guild_groups:
                    has_guild = guild_groups[sgid]
                elif sgid == generic_world or sgid == generic_guild:
                    groups_exist = True
                    break

            # User was already migrated (manually?)
            if groups_exist:
                logging.info(
                    "Skipping cldbid:%s, generic groups exist already", cldbid)
                continue

            try:
                if has_guild:
                    logging.info("Adding cldbid:%s to generic GUILD due to %s",
                                 cldbid, has_guild)
                    bot.exec_("servergroupaddclient",
                              sgid=generic_guild,
                              cldbid=cldbid)
                elif has_linked_world:
                    logging.info(
                        "Adding cldbid:%s to generic WORLD due to %s",
                        cldbid,
                        has_linked_world,
                    )
                    bot.exec_("servergroupaddclient",
                              sgid=generic_world,
                              cldbid=cldbid)
            except ts3.TS3Error:
                logging.exception("Failed to add user to generic group")

        # Skip to next user block
        start += len(users)
        try:
            users = bot.exec_("clientdblist", start=start, duration=200)
        except ts3.query.TS3QueryError as e:
            # Fetching users failed, most likely at end
            if e.args[0].error["id"] != "1281":
                logging.exception("Error retrieving user list")
            users = []

    logging.info("--- Done ---")
Beispiel #12
0
        "source_database",
        help="Use a URI of the following schema: mysql+mysqldb://"
        "<user>:<password>@<host>[:<port>]/<dbname>. "
        "Requires sqlalchemy[mysql]",
    )
    sub.add_parser("bot", help="Runs the main bot")

    args = parser.parse_args()

    session: typing.Optional[Session] = None
    if args.mode in ["bot", "cycle", "migrate"]:
        session = create_session(Config.get("database", "uri"))

    if args.mode == "bot":
        init_logger("bot")
        Bot(session).loop()
    elif args.mode == "cycle":
        init_logger("cycle")
        Cycle(
            session,
            verify_all=args.all,
            verify_linked_worlds=args.relink,
            verify_ts3=args.ts3,
            verify_world=args.world,
        ).run()
    elif args.mode == "migrate":
        init_logger("migrate")
        legacy.apply_generic_groups(session)
        legacy.migrate_database(session, args.source_database)
    else:
        parser.print_help()
Beispiel #13
0
def handle(bot: Bot, event: events.TextMessage, match: Match) -> None:
    key = match.group(1)

    # Check with ArenaNet's API
    try:
        account_info = fetch_api("account", api_key=key)

        # Check if one of the guilds is in the alliance
        is_part_of_alliance = (bot.session.query(models.Guild).filter(
            models.Guild.guid.in_(account_info.get("guilds", []))).filter(
                models.Guild.is_part_of_alliance.is_(True)).count() > 0)

        # One of the guilds is in the alliance
        if is_part_of_alliance:
            account: models.Account = models.Account.get_or_create(
                bot.session, account_info, key)
            identity: models.Identity = models.Identity.get_or_create(
                bot.session, event.uid)

            # Check if account is registered to anyone
            linked_identity: Optional[
                models.
                LinkAccountIdentity] = account.valid_identities.one_or_none()

            # Account is already linked
            if linked_identity:
                # Account is linked to another guid
                if linked_identity.identity.guid != event.uid:
                    try:
                        # Get user's DB id
                        cldbid: str = bot.exec_("clientgetdbidfromuid",
                                                cluid=event.uid)[0]["cldbid"]
                    except ts3.TS3Error:
                        LOG.error("Failed to get user's dbid", exc_info=True)
                        bot.send_message(event.id, "error_critical")
                        return

                    force_key_name = f"ts3bot-{cldbid}"

                    # Fetch token info
                    token_info = fetch_api("tokeninfo", api_key=key)

                    # Override registration, same as !register
                    if token_info.get("name", "") == force_key_name:
                        ts3bot.transfer_registration(bot, account, event)

                        LOG.info(
                            "%s (%s) transferred permissions of %s onto themselves.",
                            event.name,
                            event.uid,
                            account_info.get("name"),
                        )
                        return

                    LOG.warning(
                        "%s (%s) tried to use an already registered API key/account. (%s)",
                        event.name,
                        event.uid,
                        account_info.get("name"),
                    )
                    bot.send_message(event.id,
                                     "token_in_use",
                                     api_name=force_key_name)
                else:  # Account is linked to current guid
                    LOG.info(
                        "User %s (%s) tried to register a second time for whatever reason using %s",
                        event.name,
                        event.uid,
                        account_info.get("name", "Unknown account"),
                    )

                    # Save new API key
                    if account.api_key != key:
                        account.api_key = key
                        account.is_valid = True
                        bot.session.commit()
                        bot.send_message(event.id, "registration_exists")
                        return

                    # Same API key supplied, last check was over 12 minutes ago
                    if (ts3bot.timedelta_hours(datetime.datetime.today() -
                                               account.last_check) >= 0.2):
                        # Update saved account info if same API key was posted again with a reasonable time frame
                        account.update(bot.session)
                        try:
                            # Get user's DB id
                            cldbid = bot.exec_("clientgetdbidfromuid",
                                               cluid=event.uid)[0]["cldbid"]

                            # Sync groups
                            ts3bot.sync_groups(bot, cldbid, account)
                            bot.send_message(event.id,
                                             "registration_details_updated")
                        except ts3.TS3Error:
                            # User might not exist in the db
                            LOG.error("Failed to sync user", exc_info=True)
                    else:
                        # Too early
                        bot.send_message(event.id, "registration_too_early")

            else:
                # Otherwise account is not yet linked and can be used

                # Save API key
                account.api_key = key
                account.is_valid = True
                bot.session.commit()

                # Get user's DB id
                cldbid = bot.exec_("clientgetdbidfromuid",
                                   cluid=event.uid)[0]["cldbid"]

                # Unlink previous account from identity
                current_account = models.Account.get_by_identity(
                    bot.session, event.uid)
                if current_account:
                    LOG.info("Delinking %s from cldbid:%s", current_account,
                             cldbid)
                    current_account.invalidate(bot.session)

                # Register link between models
                bot.session.add(
                    models.LinkAccountIdentity(account=account,
                                               identity=identity))
                bot.session.commit()

                # Add all known guilds to user if enabled
                if Config.getboolean(
                        "guild", "assign_on_register") and Config.getboolean(
                            "guild", "allow_multiple_guilds"):
                    cast(AppenderQuery, account.guilds).filter(
                        models.LinkAccountGuild.id.in_(
                            bot.session.query(models.LinkAccountGuild.id).join(
                                models.Guild).filter(
                                    models.Guild.group_id.isnot(
                                        None)).subquery())).update(
                                            {"is_active": True},
                                            synchronize_session="fetch")
                    bot.session.commit()

                # Sync groups
                sync_groups(bot, cldbid, account)

                LOG.info(
                    "Assigned alliance permissions to %s (%s) using %s",
                    event.name,
                    event.uid,
                    account_info.get("name", "Unknown account"),
                )

                # Was registered with other account previously
                if current_account:
                    bot.send_message(event.id,
                                     "registration_update",
                                     account=account.name)
                else:
                    bot.send_message(event.id, "welcome_registered")

                    # Tell user about !guild if it's enabled
                    if Config.getboolean("commands", "guild"):
                        if Config.getboolean(
                                "guild",
                                "assign_on_register") and Config.getboolean(
                                    "guild", "allow_multiple_guilds"):
                            bot.send_message(event.id, "welcome_registered_3")
                        else:
                            bot.send_message(event.id, "welcome_registered_2")

        else:
            bot.send_message(
                event.id,
                "invalid_world",
                world=enums.World(account_info.get("world")).proper_name,
            )

    except InvalidKeyException:
        LOG.info("This seems to be an invalid API key.")
        bot.send_message(event.id, "invalid_token_retry")
    except (RateLimitException, RequestException, ApiErrBadData):
        bot.send_message(event.id, "error_api")
Beispiel #14
0
def handle(bot: Bot, event: events.TextMessage, match: typing.Match):
    if event.uid not in Config.whitelist_admin:
        return

    try:
        json = fetch_api("account", api_key=match.group(1))
        account = models.Account.get_by_api_info(
            bot.session, guid=json.get("id"), name=json.get("name")
        )

        # Account does not exist
        if not account:
            logging.info("User was not registered.")
            bot.send_message(event.id, "account_unknown", account=json.get("name"))
            return

        # Get previous identity
        previous_identity: typing.Optional[
            models.LinkAccountIdentity
        ] = account.valid_identities.one_or_none()

        # Remove previous links
        account.invalidate(bot.session)

        if previous_identity:
            # Get cldbid and sync groups
            try:
                cldbid = bot.exec_(
                    "clientgetdbidfromuid", cluid=previous_identity.identity.guid
                )[0]["cldbid"]

                result = sync_groups(bot, cldbid, account, remove_all=True)

                logging.info(
                    "%s (%s) marked previous links of %s as ignored",
                    event.name,
                    event.uid,
                    account.name,
                )

                bot.send_message(
                    event.id,
                    "groups_revoked",
                    amount="1",
                    groups=result["removed"],
                )
            except ts3.TS3Error:
                # User might not exist in the db
                logging.info("Failed to remove groups from user", exc_info=True)

        else:
            bot.send_message(event.id, "groups_revoked", amount="0", groups=[])
    except InvalidKeyException:
        logging.info("This seems to be an invalid API key.")
        bot.send_message(event.id, "invalid_token")
    except ApiErrBadData:
        bot.send_message(event.id, "error_api")
Beispiel #15
0
def sync_groups(
    bot: ts3_bot.Bot,
    cldbid: str,
    account: typing.Optional[ts3bot.database.models.Account],
    remove_all=False,
    skip_whitelisted=False,
) -> SyncGroupChanges:
    def sg_dict(_id, _name):
        return {"sgid": _id, "name": _name}

    def _add_group(group: ServerGroup):
        """
        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)
            logging.info("Added user dbid:%s to group %s", cldbid,
                         group["name"])
            server_group_ids.append(int(group["sgid"]))
            group_changes["added"].append(group["name"])
            return True
        except ts3.TS3Error:
            # User most likely doesn't have the group
            logging.exception(
                "Failed to add cldbid:%s to group %s for some reason.",
                cldbid,
                group["name"],
            )

    def _remove_group(group: ServerGroup):
        """
        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)
                logging.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
                logging.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: typing.List[
            ts3bot.database.models.LinkAccountGuild] = account.guild_groups()
        valid_world_group: typing.Optional[
            ts3bot.database.models.WorldGroup] = account.world_group(
                bot.session)
    else:
        valid_guild_groups = []
        valid_world_group = None

    valid_guild_group_ids = [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
    world_groups: typing.List[int] = [
        _.group_id
        for _ in bot.session.query(ts3bot.database.models.WorldGroup).options(
            load_only(ts3bot.database.models.WorldGroup.group_id))
    ]
    guild_groups: typing.List[int] = [
        _.group_id
        for _ in bot.session.query(ts3bot.database.models.Guild).filter(
            ts3bot.database.models.Guild.group_id.isnot(None)).options(
                load_only(ts3bot.database.models.Guild.group_id))
    ]
    generic_world = {
        "sgid": int(Config.get("teamspeak", "generic_world_id")),
        "name": "Generic World",
    }
    generic_guild = {
        "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_world
                or sgid == generic_guild or
            (len(valid_guild_groups) > 0 and sgid in valid_guild_group_ids)
                or (valid_world_group and sgid == valid_world_group.group_id)):
            continue

        # Skip users with whitelisted group
        if skip_whitelisted and server_group.get(
                "name") in Config.whitelist_groups:
            logging.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 and sgid not in world_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(
                sg_dict(group_id, valid_guild_mapper[group_id].guild.name))

    # Join guilds
    if len(valid_guild_group_ids) > 0:
        for group_id in joined_guilds:
            _add_group(
                sg_dict(group_id, valid_guild_mapper[group_id].guild.name))

    # User is missing generic world
    if (valid_world_group and valid_world_group.is_linked
            and generic_world["sgid"] not in server_group_ids
            and len(valid_guild_group_ids) == 0):
        _add_group(generic_world)

    # User has generic world but shouldn't
    if generic_world["sgid"] in server_group_ids and (
            not valid_world_group or not valid_world_group.is_linked
            or len(valid_guild_group_ids) > 0):
        _remove_group(generic_world)

    # User is missing home world
    if valid_world_group and valid_world_group.group_id not in server_group_ids:
        _add_group(
            sg_dict(valid_world_group.group_id,
                    valid_world_group.world.proper_name))

    return group_changes
Beispiel #16
0
def transfer_registration(
    bot: ts3_bot.Bot,
    account: models.Account,
    event: events.TextMessage,
    is_admin: bool = False,
    target_identity: typing.Optional[models.Identity] = None,
    target_dbid: typing.Optional[str] = None,
):
    """
    Transfers a registration and server/guild groups to the sender of the event or the target_guid
    :param bot: The current bot instance
    :param account: The account that should be re-registered for the target user
    :param event: The sender of the text message, usually the one who gets permissions
    :param is_admin: Whether the sender is an admin
    :param target_identity: To override the user who gets the permissions
    :param target_dbid: The target's database id, usually sourced from the event
    :return:
    """

    # Get identity from event if necessary
    if not target_identity:
        target_identity: models.Identity = models.Identity.get_or_create(
            bot.session, event.uid)

    # Get database id if necessary
    if not target_dbid:
        try:
            target_dbid: str = bot.exec_("clientgetdbidfromuid",
                                         cluid=event.uid)[0]["cldbid"]
        except ts3.TS3Error:
            # User might not exist in the db
            logging.exception("Failed to get database id from event's user")
            bot.send_message(event.id, "error_critical")
            return

    # Get current guild groups to save them for later use
    guild_groups = account.guild_groups()

    # Get previous identity
    previous_identity: typing.Optional[
        models.LinkAccountIdentity] = account.valid_identities.one_or_none()

    # Remove previous identities, also removes guild groups
    account.invalidate(bot.session)

    # Account is currently registered, sync groups with old identity
    if previous_identity:
        # Get cldbid and sync groups
        try:
            cldbid = bot.exec_(
                "clientgetdbidfromuid",
                cluid=previous_identity.identity.guid)[0]["cldbid"]

            result = sync_groups(bot, cldbid, account, remove_all=True)

            logging.info(
                "Removed previous links of %s as ignored during transfer to %s",
                account.name,
                target_identity.guid,
            )

            if is_admin:
                bot.send_message(event.id,
                                 "groups_revoked",
                                 amount="1",
                                 groups=result["removed"])
        except ts3.TS3Error:
            # User might not exist in the db
            logging.info("Failed to remove groups from user", exc_info=True)

    # Invalidate target identity's link, if it exists
    other_account = models.Account.get_by_identity(bot.session,
                                                   target_identity.guid)
    if other_account:
        other_account.invalidate(bot.session)

    # Transfer roles to new identity
    bot.session.add(
        models.LinkAccountIdentity(account=account, identity=target_identity))

    # Add guild group
    if guild_groups:
        bot.session.query(models.LinkAccountGuild).filter(
            models.LinkAccountGuild.id.in_([g.id for g in guild_groups
                                            ])).update({"is_active": True})

    bot.session.commit()

    # Sync group
    sync_groups(bot, target_dbid, account)

    logging.info("Transferred groups of %s to cldbid:%s", account.name,
                 target_dbid)

    bot.send_message(
        event.id,
        "registration_transferred",
        account=account.name,
    )
Beispiel #17
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
Beispiel #18
0
def handle(bot: Bot, event: events.TextMessage, match: typing.Match):
    cldbid = bot.exec_("clientgetdbidfromuid", cluid=event.uid)[0]["cldbid"]
    user_groups = bot.exec_("servergroupsbyclientid", cldbid=cldbid)
    allowed = False

    if event.uid in Config.whitelist_admin:
        allowed = True
    else:
        for group in user_groups:
            if group["name"] in Config.whitelist_group_list:
                allowed = True
                break

    # User doesn't have any whitelisted groups
    if not allowed:
        return

    groups = bot.exec_("servergrouplist")
    group = None
    search_group = match.group(1).strip()
    for _ in groups:
        if _["type"] != "1":  # Regular type, neither template nor query
            continue

        if _["name"] == search_group:
            group = _
            break

    # Group not found
    if group is None:
        bot.send_message(event.id, "list_not_found")
        return

    members = bot.exec_("servergroupclientlist", "names", sgid=group["sgid"])

    members = sorted(members, key=lambda _: _["client_nickname"])

    if len(members) >= 50:
        bot.send_message(event.id, "list_50_users")
        return

    text_groups = [""]
    index = 0
    for member in members:
        member_text = "\n- [URL=client://0/{}]{}[/URL]".format(
            member["client_unique_identifier"], member["client_nickname"])
        if len(text_groups[index]) + len(bytes(member_text, "utf-8")) >= 1024:
            index += 1
            text_groups.append("")

        text_groups[index] += member_text

    bot.send_message(event.id,
                     "list_users",
                     amount=len(members),
                     group=group["name"])
    for _ in text_groups:
        bot.send_message(event.id, _, is_translation=False)
Beispiel #19
0
def handle(bot: Bot, event: events.TextMessage, match: Match) -> None:
    if event.uid not in Config.whitelist_admin:
        return

    # Grab cluid
    try:
        if match.group(1).isdigit():  # DB id
            user = bot.exec_("clientgetnamefromdbid", cldbid=match.group(1))
            cldbid = match.group(1)
            cluid = user[0]["cluid"]
        else:
            user = bot.exec_("clientgetnamefromuid", cluid=match.group(1))
            cldbid = user[0]["cldbid"]
            cluid = match.group(1)
    except ts3.query.TS3QueryError:
        bot.send_message(event.id, "user_not_found")
        return

    # Grab user's account
    account = models.Account.get_by_identity(bot.session, cluid)

    if not account:
        bot.send_message(event.id, "verify_no_token")
        return

    try:
        bot.send_message(event.id, "account_updating")
        result = account.update(bot.session)
        if result["transfer"]:
            old_world: enums.World = result["transfer"][0]
            new_world: enums.World = result["transfer"][1]

            bot.send_message(
                event.id,
                "verify_transferred",
                old_world=old_world.proper_name,
                new_world=new_world.proper_name,
            )
        guilds_joined, guilds_left = result["guilds"]

        if len(guilds_joined) > 0 or len(guilds_left) > 0:
            bot.send_message(
                event.id,
                "verify_guild_change",
                guilds_joined=guilds_joined,
                guilds_left=guilds_left,
            )

        # Sync user's groups
        sync_groups(bot, cldbid, account)

        bot.send_message(
            event.id,
            "verify_valid_world",
            user=account.name,
            world=account.world.proper_name,
        )
    except InvalidKeyException:
        bot.send_message(event.id, "invalid_token")

        # Invalidate link
        account.invalidate(bot.session)
        changes = sync_groups(bot, cldbid, account)

        bot.send_message(event.id, "groups_removed", groups=str(changes["removed"]))
    except (requests.RequestException, ApiErrBadData):
        bot.send_message(event.id, "error_api")