@@ -41,6 +41,10 @@ PWN_COLLEGE_GITHUB_ORG = "pwncollege"
4141TEXT_CHANNEL_TYPES = {0 , 5 }
4242PUBLIC_THREAD_TYPES = {10 , 11 }
4343THREAD_PARENT_TYPES = {0 , 5 , 15 , 16 }
44+ # Keep ordinary posts, replies, application-command responses, thread starters, and context
45+ # menu responses. Other Discord message types are generated system events rather than
46+ # learner-authored discussion.
47+ FEEDBACK_MESSAGE_TYPES = {0 , 19 , 20 , 21 , 23 }
4448MAX_DISCORD_PAGE_SIZE = 100
4549MAX_DISCORD_SERVER_ERROR_RETRIES = 5
4650# Image attachments are downloaded so the analysis agent can view screenshots that carry
@@ -640,11 +644,10 @@ def fetch_messages_since(
640644 api : DiscordAPI ,
641645 channel : dict [str , Any ],
642646 cutoff : datetime .datetime ,
643- max_messages : int ,
644647) -> list [dict [str , Any ]]:
645648 messages : list [dict [str , Any ]] = []
646649 params : dict [str , Any ] = {"limit" : MAX_DISCORD_PAGE_SIZE }
647- while len ( messages ) < max_messages :
650+ while True :
648651 try :
649652 page = api .request (f"/channels/{ channel ['id' ]} /messages" , params )
650653 except (DiscordPermissionError , DiscordServerError ) as error :
@@ -667,13 +670,51 @@ def fetch_messages_since(
667670 oldest_time = (
668671 message_time if oldest_time is None else min (oldest_time , message_time )
669672 )
670- if message_time >= cutoff :
673+ if message_time >= cutoff and message_has_feedback_content ( message ) :
671674 messages .append (message )
672675
673676 if oldest_id is None or oldest_time is None or oldest_time < cutoff :
674- return messages [: max_messages ]
677+ return messages
675678 params ["before" ] = str (oldest_id )
676- return messages [:max_messages ]
679+
680+
681+ def message_has_feedback_content (message : dict [str , Any ]) -> bool :
682+ """Whether a Discord message carries content useful to the feedback analysis.
683+
684+ Discord system events such as member joins have an author and timestamp but no actual
685+ message payload. Do not let those empty events crowd learner feedback out of the global
686+ retention limit.
687+ """
688+ if message .get ("type" , 0 ) not in FEEDBACK_MESSAGE_TYPES :
689+ return False
690+ return bool (
691+ (message .get ("content" ) or "" ).strip ()
692+ or message .get ("attachments" )
693+ or message .get ("embeds" )
694+ )
695+
696+
697+ def retain_most_recent_messages (
698+ messages_by_channel : list [tuple [dict [str , Any ], list [dict [str , Any ]]]],
699+ max_messages : int ,
700+ ) -> list [tuple [dict [str , Any ], list [dict [str , Any ]]]]:
701+ """Apply the message limit globally after every channel has been scanned."""
702+ candidates = [
703+ (channel , message )
704+ for channel , messages in messages_by_channel
705+ for message in messages
706+ ]
707+ candidates .sort (
708+ key = lambda item : (parse_iso8601 (item [1 ]["timestamp" ]), int (item [1 ]["id" ])),
709+ reverse = True ,
710+ )
711+
712+ retained_by_channel : OrderedDict [
713+ str , tuple [dict [str , Any ], list [dict [str , Any ]]]
714+ ] = OrderedDict ()
715+ for channel , message in candidates [:max_messages ]:
716+ retained_by_channel .setdefault (channel ["id" ], (channel , []))[1 ].append (message )
717+ return list (retained_by_channel .values ())
677718
678719
679720def sanitize_content (content : str , channels_by_id : dict [str , dict [str , Any ]]) -> str :
@@ -4432,14 +4473,10 @@ def feedback_command(
44324473 channels , channels_by_id = discover_channels (
44334474 api , guild_id , selected_channel_ids , since , include_threads
44344475 )
4435- click .echo (f"Discovered { len (channels )} readable channel(s)/thread(s)" )
4476+ click .echo (f"Discovered { len (channels )} channel(s)/thread(s) to scan " )
44364477 messages_by_channel : list [tuple [dict [str , Any ], list [dict [str , Any ]]]] = []
4437- remaining = max_messages
44384478 for channel in channels :
4439- if remaining <= 0 :
4440- break
4441- messages = fetch_messages_since (api , channel , since , remaining )
4442- remaining -= len (messages )
4479+ messages = fetch_messages_since (api , channel , since )
44434480 if messages :
44444481 messages_by_channel .append ((channel , messages ))
44454482 click .echo (
@@ -4453,6 +4490,17 @@ def feedback_command(
44534490 channel_display_name (channel , channels_by_id ),
44544491 )
44554492
4493+ matching_message_count = sum (
4494+ len (channel_messages ) for _channel , channel_messages in messages_by_channel
4495+ )
4496+ messages_by_channel = retain_most_recent_messages (
4497+ messages_by_channel , max_messages
4498+ )
4499+ click .echo (
4500+ f"Scanned all { len (channels )} channel(s)/thread(s); retaining the most "
4501+ f"recent { min (matching_message_count , max_messages )} of "
4502+ f"{ matching_message_count } non-empty message(s)"
4503+ )
44564504 messages = sanitize_messages (
44574505 messages_by_channel ,
44584506 channels_by_id ,
@@ -4513,6 +4561,8 @@ def feedback_command(
45134561 "since" : since .isoformat (),
45144562 "until" : until .isoformat (),
45154563 "messages" : len (messages ),
4564+ "matching_messages" : matching_message_count ,
4565+ "max_messages" : max_messages ,
45164566 "image_attachments" : image_count ,
45174567 "channels" : len (channels ),
45184568 "include_threads" : include_threads ,
0 commit comments