There are two types of hooks available to plugins:
Events
Can modify data before Nicotine+ processes it. Time-critical - must execute quickly to avoid freezing the UI. Return modified arguments or return codes to control flow.
Notifications
Cannot modify data. Receive information about what happened. Can take as long as needed without blocking the UI. Preferred for most use cases.
Events are time-critical. Every millisecond spent in event handlers delays the UI. Only use events when you need to modify or block data - prefer notifications for everything else.
Event handlers can return special codes to control how Nicotine+ processes the event:
from pynicotine.pluginsystem import returncode# Available return codes:returncode["break"] # 0 - Don't give event to other plugins, DO let Nicotine+ process itreturncode["zap"] # 1 - Don't give event to other plugins, DON'T let Nicotine+ process it returncode["pass"] # 2 - DO give event to other plugins, DO let Nicotine+ process it# Returning None or nothing is the same as "pass"
def incoming_public_chat_event(self, room, user, line): """Modify incoming chat room message before processing. Args: room (str): Chat room name user (str): Username who sent the message line (str): Message text Returns: tuple: Modified (room, user, line) or return code """ return room, user, line.upper() # Convert to uppercase
Use case: Filter spam, modify message display, implement custom formatting
def incoming_public_chat_notification(self, room, user, line): """Notification of incoming chat room message. Args: room (str): Chat room name user (str): Username who sent the message line (str): Message text """ self.log(f"{user} said in {room}: {line}")
Use case: Log messages, trigger actions based on chat content
def incoming_private_chat_event(self, user, line): """Modify incoming private message before processing. Args: user (str): Username who sent the message line (str): Message text Returns: tuple: Modified (user, line) or return code """ return user, line
Use case: Filter private messages, auto-respond to queries
def incoming_private_chat_notification(self, user, line): """Notification of incoming private message. Args: user (str): Username who sent the message line (str): Message text """ pass
def outgoing_public_chat_event(self, room, line): """Modify outgoing chat room message before sending. Args: room (str): Chat room name line (str): Message text to send Returns: tuple: Modified (room, line) or return code """ return room, line
Use case: Add signatures, replace emoticons, implement shortcuts
def outgoing_public_chat_notification(self, room, line): """Notification of outgoing chat room message. Args: room (str): Chat room name line (str): Message text sent """ pass
def outgoing_private_chat_event(self, user, line): """Modify outgoing private message before sending. Args: user (str): Recipient username line (str): Message text to send Returns: tuple: Modified (user, line) or return code """ return user, line
def outgoing_private_chat_notification(self, user, line): """Notification of outgoing private message. Args: user (str): Recipient username line (str): Message text sent """ pass
def user_join_chatroom_notification(self, room, user): """Another user joined a chat room. Args: room (str): Room name user (str): Username who joined """ pass
def user_leave_chatroom_notification(self, room, user): """Another user left a chat room. Args: room (str): Room name user (str): Username who left """ pass
def public_room_message_notification(self, room, user, line): """Public room message received (after processing). Args: room (str): Room name user (str): Username line (str): Message text """ pass
def private_room_membership_granted_notification(self, room): """You were granted membership to a private room. Args: room (str): Private room name """ pass
def private_room_membership_revoked_notification(self, room): """Your membership to a private room was revoked. Args: room (str): Private room name """ pass
def private_room_member_added_notification(self, room, user): """User was added as member to a private room. Args: room (str): Private room name user (str): Username added """ # Example: Auto-add room members as buddies if user != self.core.users.login_username: self.core.buddies.add_buddy(user)
def private_room_operatorship_granted_notification(self, room): """You were granted operator status in a private room. Args: room (str): Private room name """ pass
def private_room_operatorship_revoked_notification(self, room): """Your operator status was revoked in a private room. Args: room (str): Private room name """ pass
def private_room_operator_added_notification(self, room, user): """User was granted operator status in a private room. Args: room (str): Private room name user (str): Username granted operator """ pass
def private_room_operator_removed_notification(self, room, user): """User's operator status was revoked in a private room. Args: room (str): Private room name user (str): Username with revoked operator """ pass
def outgoing_global_search_event(self, text): """Modify global search query before sending. Args: text (str): Search text Returns: tuple: Modified (text,) or return code """ return (text,)
def outgoing_room_search_event(self, rooms, text): """Modify room search before sending. Args: rooms (list): List of room names to search text (str): Search text Returns: tuple: Modified (rooms, text) or return code """ return rooms, text
def outgoing_user_search_event(self, users, text): """Modify user search before sending. Args: users (list): List of usernames to search text (str): Search text Returns: tuple: Modified (users, text) or return code """ return users, text
def upload_queued_notification(self, user, virtual_path, real_path): """Upload was queued. Args: user (str): Username requesting upload virtual_path (str): Virtual file path seen by user real_path (str): Real file path on disk """ pass
def server_disconnect_notification(self, userchoice): """Disconnected from Soulseek server. Args: userchoice (bool): True if user initiated disconnect """ if not userchoice: self.log("Connection lost!")
def user_resolve_notification(self, user, ip_address, port, country): """User IP address and port were resolved. Args: user (str): Username ip_address (str): IP address port (int): Port number country (str): Country code (only set when user requested resolving) """ self.log(f"{user} is at {ip_address}:{port} ({country})")
def user_status_notification(self, user, status, privileged): """User status changed. Args: user (str): Username status (int): Status code (UserStatus enum) privileged (bool): Whether user has privileges """ pass
The ResponseThrottle class helps avoid chat flooding when responding to user requests:
from pynicotine.pluginsystem import BasePlugin, ResponseThrottleclass Plugin(BasePlugin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.throttle = ResponseThrottle(self.core, self.human_name, logging=True) def incoming_public_chat_event(self, room, user, line): if line.lower() == "!info": # Check if it's okay to respond (prevents spam/ban) if self.throttle.ok_to_respond(room, user, line, seconds_limit_min=30): self.throttle.responded() # Mark that we responded self.send_public(room, "Here's the info you requested...")
The Soulseek server can temporarily ban you from chat rooms if you send messages too quickly. Always use ResponseThrottle when building auto-responder plugins.