Пример #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()
Пример #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()
Пример #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()],
        )
Пример #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)
Пример #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()
Пример #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)
Пример #7
0
        help=
        "Migrates the old database (<2020) to the current version. Uses cycle account",
    )
    sub_migrate.add_argument(
        "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")
Пример #8
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))
Пример #9
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
Пример #10
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
Пример #11
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
Пример #12
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