@@ -484,6 +484,13 @@ def _download_and_extract_github(
484484 async def _apply_template_from_dir (
485485 self , guild , template_dir , ctx = None , interaction = None
486486 ):
487+ """Apply template from directory - now looks for monolithic template.yaml first, falls back to legacy format."""
488+ # Try monolithic template format first
489+ template_path = os .path .join (template_dir , "template.yaml" )
490+ if os .path .exists (template_path ):
491+ return await self ._apply_monolithic_template (guild , template_path , ctx , interaction )
492+
493+ # Fall back to legacy directory-based format
487494 result_msgs = []
488495 template_category_names = set ()
489496 template_channel_names = set ()
@@ -671,6 +678,175 @@ async def _apply_template_from_dir(
671678 result_msgs .append (msg )
672679 return result_msgs
673680
681+ async def _apply_monolithic_template (self , guild , template_path , ctx = None , interaction = None ):
682+ """Apply a monolithic template.yaml file to the guild."""
683+ from ..utils .helpers import parse_monolithic_template
684+
685+ result_msgs = []
686+ template_category_names = set ()
687+
688+ try :
689+ template_config = parse_monolithic_template (template_path )
690+ except Exception as e :
691+ msg = f"❌ Failed to parse template: { e } "
692+ self .logger .error (f"[apply_monolithic_template] { msg } " )
693+ result_msgs .append (msg )
694+ return result_msgs
695+
696+ # Log template info if available
697+ if "server" in template_config :
698+ server_info = template_config ["server" ]
699+ if "name" in server_info :
700+ msg = f"📋 Applying template: { server_info ['name' ]} "
701+ if "version" in server_info :
702+ msg += f" v{ server_info ['version' ]} "
703+ self .logger .info (f"[apply_monolithic_template] { msg } " )
704+ result_msgs .append (msg )
705+
706+ # Process each category
707+ for category_config in template_config ["categories" ]:
708+ category_name = category_config ["name" ]
709+ template_category_names .add (category_name )
710+
711+ # Create or update the category
712+ existing_category = discord .utils .get (guild .categories , name = category_name )
713+ if existing_category :
714+ category = existing_category
715+ msg = f"ℹ️ Category '{ category_name } ' already exists. Will update channels."
716+ else :
717+ category = await guild .create_category (
718+ name = category_name , position = category_config .get ("position" , 0 )
719+ )
720+ msg = f"✅ Created category: { category_name } "
721+
722+ self .logger .info (f"[apply_monolithic_template] { msg } " )
723+ result_msgs .append (msg )
724+
725+ # Process channels in this category
726+ created , updated , skipped = 0 , 0 , 0
727+ template_channel_names = set ()
728+
729+ channels = category_config .get ("channels" , [])
730+ for channel_config in channels :
731+ channel_name = channel_config ["name" ]
732+ template_channel_names .add (channel_name )
733+
734+ # Check if channel exists
735+ existing_channel = discord .utils .get (category .channels , name = channel_name )
736+ channel_type = channel_config ["type" ].lower ()
737+
738+ if existing_channel :
739+ # Update topic/nsfw/position if needed
740+ update_kwargs = {}
741+ if (
742+ channel_type == "text"
743+ and hasattr (existing_channel , "topic" )
744+ and existing_channel .topic != channel_config .get ("topic" , "" )
745+ ):
746+ update_kwargs ["topic" ] = channel_config .get ("topic" , "" )
747+ if (
748+ channel_type in ("text" , "voice" )
749+ and hasattr (existing_channel , "nsfw" )
750+ and existing_channel .nsfw != channel_config .get ("nsfw" , False )
751+ ):
752+ update_kwargs ["nsfw" ] = channel_config .get ("nsfw" , False )
753+ if (
754+ "position" in channel_config
755+ and hasattr (existing_channel , "position" )
756+ and existing_channel .position != channel_config ["position" ]
757+ ):
758+ update_kwargs ["position" ] = channel_config ["position" ]
759+
760+ if update_kwargs :
761+ await existing_channel .edit (** update_kwargs )
762+ updated += 1
763+ msg = f"🔄 Updated channel: { existing_channel .name } in { category_name } "
764+ else :
765+ skipped += 1
766+ msg = f"⏭️ Skipped channel (no changes): { existing_channel .name } in { category_name } "
767+
768+ self .logger .info (f"[apply_monolithic_template] { msg } " )
769+ result_msgs .append (msg )
770+ else :
771+ # Create new channel
772+ channel_kwargs = {
773+ "name" : channel_config ["name" ],
774+ "category" : category ,
775+ }
776+ if "position" in channel_config :
777+ channel_kwargs ["position" ] = channel_config ["position" ]
778+
779+ if channel_type == "text" :
780+ if "topic" in channel_config :
781+ channel_kwargs ["topic" ] = channel_config ["topic" ]
782+ if "nsfw" in channel_config :
783+ channel_kwargs ["nsfw" ] = channel_config ["nsfw" ]
784+ await guild .create_text_channel (** channel_kwargs )
785+ elif channel_type == "voice" :
786+ await guild .create_voice_channel (** channel_kwargs )
787+ else :
788+ msg = f"❌ Unknown channel type: { channel_type } for { channel_config ['name' ]} "
789+ self .logger .error (f"[apply_monolithic_template] { msg } " )
790+ result_msgs .append (msg )
791+ skipped += 1
792+ continue
793+
794+ created += 1
795+ msg = f"✅ Created channel: { channel_config ['name' ]} in { category_name } "
796+ self .logger .info (f"[apply_monolithic_template] { msg } " )
797+ result_msgs .append (msg )
798+
799+ # Check for extra channels in this category
800+ extra_channels = [
801+ ch for ch in category .channels if ch .name not in template_channel_names
802+ ]
803+ if extra_channels :
804+ msg = f"⚠️ Extra channels not in template for category '{ category_name } ': { ', ' .join (ch .name for ch in extra_channels )} "
805+ self .logger .warning (f"[apply_monolithic_template] { msg } " )
806+ result_msgs .append (msg )
807+ view = DeleteExtraObjectsView (extra_channels , object_type_label = "channel" )
808+ if interaction :
809+ await interaction .followup .send (msg , view = view )
810+ elif ctx :
811+ await ctx .send (msg , view = view )
812+
813+ summary = f"**{ category_name } **: { created } created, { updated } updated, { skipped } skipped"
814+ result_msgs .append (summary )
815+
816+ # Check for extra categories
817+ extra_categories = [
818+ cat for cat in guild .categories if cat .name not in template_category_names
819+ ]
820+ if extra_categories :
821+ msg = f"⚠️ Extra categories not in template: { ', ' .join (cat .name for cat in extra_categories )} "
822+ self .logger .warning (f"[apply_monolithic_template] { msg } " )
823+ result_msgs .append (msg )
824+ view = DeleteExtraObjectsView (extra_categories , object_type_label = "category" )
825+ if interaction :
826+ await interaction .followup .send (msg , view = view )
827+ elif ctx :
828+ await ctx .send (msg , view = view )
829+
830+ # Check for orphan channels
831+ orphan_channels = []
832+ for ch in guild .channels :
833+ if getattr (ch , "category" , None ) is None and isinstance (
834+ ch , (discord .TextChannel , discord .VoiceChannel )
835+ ):
836+ orphan_channels .append (ch )
837+
838+ if orphan_channels :
839+ msg = f"⚠️ Uncategorized channels not in template: { ', ' .join (ch .name for ch in orphan_channels )} "
840+ self .logger .warning (f"[apply_monolithic_template] { msg } " )
841+ result_msgs .append (msg )
842+ view = DeleteExtraObjectsView (orphan_channels , object_type_label = "channel" )
843+ if interaction :
844+ await interaction .followup .send (msg , view = view )
845+ elif ctx :
846+ await ctx .send (msg , view = view )
847+
848+ return result_msgs
849+
674850
675851async def setup (bot : commands .Bot ) -> None :
676852 """Set up the Admin cog."""
0 commit comments