feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: add user whitelist security module - #19

Merged
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security
Jan 11, 2026
Merged

feat: add user whitelist security module#19
BukeLy merged 2 commits into
mainfrom
feat/user-whitelist-security

Conversation

@BukeLy

Copy link
Copy Markdown
Owner

Summary

  • Add security.py module for Telegram Bot access control
  • Support my_chat_member event to auto-leave groups when added by unauthorized users
  • Block private messages from non-whitelisted users
  • Configure via [security] section in config.toml

Features

  • user_whitelist: List of allowed Telegram user IDs
  • Default ["all"] allows everyone (backward compatible)
  • Example: user_whitelist = [123456789, 987654321]

Changes

  • agent-sdk-client/security.py - New security module
  • agent-sdk-client/config.py - Add whitelist loading
  • agent-sdk-client/config.toml - Add security section
  • agent-sdk-client/handler.py - Add security checks

Test plan

  • Set whitelist to specific user ID
  • Test private message from non-whitelisted user (should be ignored)
  • Test adding bot to group by non-whitelisted user (bot should leave)
  • Test with ["all"] (should work for everyone)

记录 SDK 中 plugin 机制的实现细节
- Add security.py with user whitelist validation
- Support my_chat_member event to auto-leave unauthorized groups
- Block private messages from non-whitelisted users
- Configure via [security] section in config.toml
- Default: user_whitelist = ["all"] (allow everyone)
CopilotAI review requested due to automatic review settings January 11, 2026 09:29
@BukeLy
BukeLy merged commit ef5c48d into mainJan 11, 2026
5 checks passed
@BukeLy
BukeLy deleted the feat/user-whitelist-security branch January 11, 2026 09:37

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a user whitelist security module to control access to a Telegram bot. The feature allows bot administrators to specify which Telegram user IDs can add the bot to groups and send private messages, with a default of allowing everyone for backward compatibility.

Changes:

  • New security module with whitelist validation functions
  • Configuration system extended to load user whitelist from TOML
  • Handler updated to block unauthorized private messages and auto-leave groups added by unauthorized users

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
FileDescription
docs/authropic-agent-sdk-official/plugin-in-sdk.mdComplete documentation file about plugin loading - appears unrelated to security feature
agent-sdk-client/security.pyNew module implementing whitelist checks and group leave logic
agent-sdk-client/config.pyExtended to load and validate user_whitelist from config file with fallback defaults
agent-sdk-client/config.tomlAdded [security] section with user_whitelist configuration and documentation
agent-sdk-client/handler.pyIntegrated security checks for private messages and my_chat_member events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +77
whitelist = security.get('user_whitelist', ['all'])
if not isinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist = ['all']
else:
validated = []
for item in whitelist:
if item == 'all':
validated.append('all')
elif isinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist = validated if validated else ['all']

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitelist validation logic has an edge case issue. If a user provides a whitelist that contains only invalid entries (e.g., strings other than 'all', floats, etc.), all entries will be skipped, resulting in an empty validated list. The code then falls back to ['all'], which effectively disables security even though the user explicitly configured a whitelist. This could be a security issue as it silently fails open rather than closed. Consider either rejecting the configuration entirely with an error, or keeping the empty list to deny all access when all entries are invalid.

Suggested change
whitelist=security.get('user_whitelist', ['all'])
ifnotisinstance(whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated= []
foriteminwhitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
whitelist=validatedifvalidatedelse ['all']
raw_whitelist=security.get('user_whitelist', None)
ifraw_whitelistisNone:
# No whitelist configured; default to allow all
whitelist= ['all']
elifnotisinstance(raw_whitelist, list):
logger.warning("user_whitelist is not a list; using default ['all']")
whitelist= ['all']
else:
validated: list[int|str] = []
foriteminraw_whitelist:
ifitem=='all':
validated.append('all')
elifisinstance(item, int):
validated.append(item)
else:
logger.warning(f"Invalid whitelist entry: {item}; skipping")
ifnotvalidated:
# Explicit whitelist provided but no valid entries; deny all access
logger.warning(
"user_whitelist is empty after validation; denying all access"
)
whitelist=validated

Copilot uses AI. Check for mistakes.
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only checks for status transitions from 'left' or 'kicked' to 'member' or 'administrator', but doesn't handle the case where the bot's status is upgraded from 'member' to 'administrator' by someone other than the original inviter. If an unauthorized user promotes the bot to admin in a group where it was invited by an authorized user, the bot won't leave. Consider whether permission changes should also be validated against the whitelist.

Suggested change
# Bot being promoted to administrator by a (potentially) unauthorized user
ifold_statusnotin ('administrator', 'creator') andnew_status=='administrator':
inviter_id=member_update.from_user.id
returnnotis_user_allowed(inviter_id, whitelist)

Copilot uses AI. Check for mistakes.
if should_leave_group(update, config.user_whitelist):
chat_id = update.my_chat_member.chat.id
inviter_id = update.my_chat_member.from_user.id
asyncio.run(bot.leave_chat(chat_id))

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using asyncio.run() in a Lambda handler that's already in a synchronous context can create problems. The bot.leave_chat() call is async, but wrapping it in asyncio.run() creates a new event loop each time. This works but is inefficient. Additionally, there's no error handling if the leave_chat operation fails (e.g., network issues, bot lacks permissions). Consider either:

  1. Wrapping the entire operation in a try-except block to handle potential exceptions
  2. Using a synchronous approach if the telegram library supports it
  3. At minimum, catching and logging any exceptions from the leave_chat call to prevent the Lambda from failing
Suggested change
asyncio.run(bot.leave_chat(chat_id))
try:
asyncio.run(bot.leave_chat(chat_id))
exceptException:
logger.exception(
"Failed to leave unauthorized group",
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
)

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +201
# Check private message whitelist
if message.chat.type == 'private':
user_id = message.from_user.id if message.from_user else None
if user_id and not is_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security whitelist check only applies to private messages (chat.type == 'private'), but group messages are not filtered by user whitelist. This means that even if a bot leaves a group when added by an unauthorized user, if the bot is added to a group by an authorized user, all members of that group can interact with the bot regardless of whether they're in the whitelist. Consider whether this is the intended behavior or if group messages should also be filtered by the sender's user_id against the whitelist.

Suggested change
# Check private message whitelist
ifmessage.chat.type=='private':
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
logger.info(
f"Blocked private message from unauthorized user",
extra={'user_id': user_id},
)
_send_metric('SecurityBlock.UnauthorizedPrivate')
return {'statusCode': 200}
# Check message whitelist (applies to both private and group chats)
user_id=message.from_user.idifmessage.from_userelseNone
ifuser_idandnotis_user_allowed(user_id, config.user_whitelist):
chat_type=getattr(message.chat, "type", None)
metric_name= (
'SecurityBlock.UnauthorizedPrivate'
ifchat_type=='private'
else'SecurityBlock.UnauthorizedGroupMessage'
)
logger.info(
"Blocked message from unauthorized user",
extra={
'user_id': user_id,
'chat_id': getattr(message.chat, "id", None),
'chat_type': chat_type,
},
)
_send_metric(metric_name)
return {'statusCode': 200}

Copilot uses AI. Check for mistakes.
else:
validated = []
for item in whitelist:
if item == 'all':

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation logic checks for the string 'all' (line 71) but doesn't validate that it's actually the string 'all' - it just checks equality. This means if someone provides user_whitelist = ["all", "ALL", "All"], only the lowercase "all" would be recognized. Consider normalizing the string to lowercase before comparison, or documenting that only lowercase "all" is valid. Similarly, the documentation in config.toml should clarify that "all" must be lowercase.

Suggested change
ifitem=='all':
ifisinstance(item, str) anditem.lower()=='all':

Copilot uses AI. Check for mistakes.

Returns:
True if user is allowed, False otherwise.
"""

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't explicitly handle the case of an empty whitelist ([]). If the whitelist is empty (which shouldn't happen due to the fallback to ['all'] in config loading, but could happen if called programmatically), the function would return False for all users, which is safe but might be unexpected. Consider adding explicit handling or documentation for this edge case.

Suggested change
"""
"""
# Explicitly handle empty whitelist: deny access to all users.
ifnotwhitelist:
returnFalse

Copilot uses AI. Check for mistakes.
Comment threaddocs/authropic-agent-sdk-official/plugin-in-sdk.md
Comment on lines +1 to +42
"""Security module for Telegram Bot access control."""
from telegram import Update


def is_user_allowed(user_id: int, whitelist: list[int | str]) -> bool:
"""Check if user is in whitelist.

Args:
user_id: Telegram user ID to check.
whitelist: List of allowed user IDs, or ['all'] to allow everyone.

Returns:
True if user is allowed, False otherwise.
"""
if 'all' in whitelist:
return True
return user_id in whitelist


def should_leave_group(update: Update, whitelist: list[int | str]) -> bool:
"""Check if bot should leave a group based on who added it.

Args:
update: Telegram Update object with my_chat_member event.
whitelist: List of allowed user IDs who can add bot to groups.

Returns:
True if bot should leave (added by unauthorized user), False otherwise.
"""
if not update.my_chat_member:
return False

member_update = update.my_chat_member
old_status = member_update.old_chat_member.status
new_status = member_update.new_chat_member.status

# Bot being added to group (status changed from left/kicked to member/administrator)
if old_status in ('left', 'kicked') and new_status in ('member', 'administrator'):
inviter_id = member_update.from_user.id
return not is_user_allowed(inviter_id, whitelist)

return False

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new security functionality lacks test coverage. The repository has comprehensive tests for config loading (test_command_config.py), but no tests are included for:

  1. The is_user_allowed function with various whitelist configurations
  2. The should_leave_group function with different chat member status transitions
  3. Integration of security checks in the handler
  4. Edge cases like empty whitelists, mixed valid/invalid entries, etc.

Consider adding tests for the new security module to ensure the whitelist logic works correctly.

Copilot uses AI. Check for mistakes.
@BukeLy

Copy link
Copy Markdown
OwnerAuthor

@copilot open a new pull request to apply changes based on the comments in this thread
but direct merge to main.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@BukeLy