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)
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()
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 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")
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")
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 ---")
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")
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
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))
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
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 ---")
"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()
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")
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")
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
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, )
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
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)
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")