Skip to content

Commit

Permalink
feat: block users (#395)
Browse files Browse the repository at this point in the history
* feat: add block user falgs

* feat: implement user blocking

* feat: remove duplicate import

* feat: fixes to typing
  • Loading branch information
JurgenR authored Jan 1, 2025
1 parent 0aa3882 commit a923076
Show file tree
Hide file tree
Showing 16 changed files with 563 additions and 151 deletions.
36 changes: 32 additions & 4 deletions docs/source/USAGE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ A list of friends can found in the settings under ``users.friends``. This list i
* Lock files depending on whether the user is in the list
* Automatically request the server to track the users in the list after logging on

Adding a friend on the fly means adding it to friends set and requesting to track the user:
To add a friend on the fly simply add it to the set:

.. code-block:: python
Expand All @@ -749,13 +749,41 @@ Adding a friend on the fly means adding it to friends set and requesting to trac
# Add a new friend to the list and track him
settings.users.friends.add(new_friend)
await client.users.track_friend(new_friend)
# Untrack the user and remove from the list
await client.users.untrack_friend(new_friend)
# Remove from the list
settings.users.friend.discard(new_friend)
Blocking Users
--------------

A list of blocked users can be found in the settings under ``users.blocked``, changes to this list
will automatically be picked up. Different flags can be used to block different actions by the user
which can be found in the :class:`.BlockingFlag` documentation. Note that when using
``BlockingFlag.UPLOADS`` all uploads to that user will be aborted. Unblocking the user will requeue
the uploads:

.. code-block:: python
from aioslsk.settings import Settings, CredentialsSettings, UsersSettings
from aioslsk.user.model import BlockingFlag
settings: Settings = Settings(
credentials=CredentialsSettings(username='my_user', password='Secret123'),
users=UsersSettings(
blocked={
'bad_user': BlockingFlag.ALL
}
)
)
client: SoulSeekClient = SoulSeekClient(settings)
new_blocked_user = 'ultra_bad_user'
# Add a new blocked user
settings.users.blocked[new_blocked_user] = BlockingFlag.ALL
Interests and Recommendations
=============================

Expand Down
20 changes: 18 additions & 2 deletions src/aioslsk/events.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from dataclasses import dataclass, field
import inspect
import logging
from typing import Any, Optional, TypeVar, TYPE_CHECKING, Union
from types import MethodType
import weakref

from .room.model import Room, RoomMessage
from .user.model import ChatMessage, User
from .user.model import BlockingFlag, ChatMessage, User
from .protocol.primitives import (
DirectoryData,
MessageDataclass,
Expand Down Expand Up @@ -590,3 +590,19 @@ class ScanCompleteEvent(InternalEvent):
"""Emitted when shares scan was completed"""
folder_count: int
file_count: int


@dataclass(frozen=True)
class FriendListChangedEvent(InternalEvent):
"""Emitted when a change was detected in the friends list"""
added: set[str] = field(default_factory=set)
removed: set[str] = field(default_factory=set)


@dataclass(frozen=True)
class BlockListChangedEvent(InternalEvent):
"""Emitted when a change was detected in the blocked list"""
changes: dict[str, tuple[BlockingFlag, BlockingFlag]]
"""List of changes. The key contains the username, the value is a tuple
containing the old and new flags.
"""
15 changes: 15 additions & 0 deletions src/aioslsk/peer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .shares.manager import SharesManager
from .transfer.interface import UploadInfoProvider
from .user.manager import UserManager
from .user.model import BlockingFlag
from .utils import ticket_generator


Expand Down Expand Up @@ -60,11 +61,15 @@ def register_listeners(self):

@on_message(PeerSharesRequest.Request)
async def _on_peer_shares_request(self, message: PeerSharesRequest.Request, connection: PeerConnection):

if not connection.username:
logger.warning(
"got PeerSharesRequest for a connection that wasn't properly initialized")
return

if self._settings.users.is_blocked(connection.username, BlockingFlag.SHARES):
return

visible, locked = self._shares_manager.create_shares_reply(connection.username)
await connection.send_message(
PeerSharesReply.Request(
Expand All @@ -75,6 +80,7 @@ async def _on_peer_shares_request(self, message: PeerSharesRequest.Request, conn

@on_message(PeerSharesReply.Request)
async def _on_peer_shares_reply(self, message: PeerSharesReply.Request, connection: PeerConnection):

if not connection.username:
logger.warning(
"got PeerSharesRequest for a connection that wasn't properly initialized")
Expand All @@ -99,11 +105,15 @@ async def _on_peer_shares_reply(self, message: PeerSharesReply.Request, connecti
@on_message(PeerDirectoryContentsRequest.Request)
async def _on_peer_directory_contents_req(
self, message: PeerDirectoryContentsRequest.Request, connection: PeerConnection):

if not connection.username:
logger.warning(
"got PeerDirectoryContentsRequest for a connection that wasn't properly initialized")
return

if self._settings.users.is_blocked(connection.username, BlockingFlag.SHARES):
return

directories = self._shares_manager.create_directory_reply(message.directory)
await connection.send_message(
PeerDirectoryContentsReply.Request(
Expand All @@ -116,6 +126,7 @@ async def _on_peer_directory_contents_req(
@on_message(PeerDirectoryContentsReply.Request)
async def _on_peer_directory_contents_reply(
self, message: PeerDirectoryContentsReply.Request, connection: PeerConnection):

if not connection.username:
logger.warning(
"got PeerDirectoryContentsReply for a connection that wasn't properly initialized")
Expand All @@ -133,11 +144,15 @@ async def _on_peer_directory_contents_reply(

@on_message(PeerUserInfoRequest.Request)
async def _on_peer_user_info_request(self, message: PeerUserInfoRequest.Request, connection: PeerConnection):

if not connection.username:
logger.warning(
"got PeerSharesRequest for a connection that wasn't properly initialized")
return

if self._settings.users.is_blocked(connection.username, BlockingFlag.INFO):
return

description = self._settings.credentials.info.description or ""
picture = self._settings.credentials.info.picture

Expand Down
10 changes: 9 additions & 1 deletion src/aioslsk/room/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from ..network.network import Network
from ..settings import Settings
from ..user.manager import UserManager
from ..user.model import UserStatus
from ..user.model import BlockingFlag, UserStatus


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -141,6 +141,10 @@ async def auto_join_rooms(self):

@on_message(RoomChatMessage.Response)
async def _on_chat_room_message(self, message: RoomChatMessage.Response, connection: ServerConnection):

if self._settings.users.is_blocked(message.username, BlockingFlag.ROOM_MESSAGES):
return

user = self._user_manager.get_user_object(message.username)
room = self.get_or_create_room(message.room)
room_message = RoomMessage(
Expand All @@ -159,6 +163,10 @@ async def _on_chat_room_message(self, message: RoomChatMessage.Response, connect

@on_message(PublicChatMessage.Response)
async def _on_public_chat_message(self, message: PublicChatMessage.Response, connection: ServerConnection):

if self._settings.users.is_blocked(message.username, BlockingFlag.ROOM_MESSAGES):
return

room = self.get_or_create_room(message.room)
user = self._user_manager.get_user_object(message.username)

Expand Down
29 changes: 26 additions & 3 deletions src/aioslsk/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from collections.abc import Set
import os
from typing import Optional
from typing import Annotated, Any, Optional, Sequence

from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, BeforeValidator, Field, model_validator
from pydantic_settings import BaseSettings

from .constants import (
Expand All @@ -11,6 +12,22 @@
)
from .network.network import ListeningConnectionErrorMode
from .shares.model import DirectoryShareMode
from .user.model import BlockingFlag


def translate_blocked_users(value: Any) -> dict[str, BlockingFlag]:
"""Converts blocked user set to a dictionary object"""
if isinstance(value, dict):
return value

if isinstance(value, (Sequence, Set)):
blocked_dict = {}
for username in value:
blocked_dict[username] = BlockingFlag.ALL

return blocked_dict

raise ValueError(f"unable to translate type {type(value)}")


class SharedDirectorySettingEntry(BaseModel, validate_assignment=True):
Expand Down Expand Up @@ -140,7 +157,13 @@ class RoomsSettings(BaseModel, validate_assignment=True):

class UsersSettings(BaseModel, validate_assignment=True):
friends: set[str] = Field(default_factory=set)
blocked: set[str] = Field(default_factory=set)
blocked: Annotated[
dict[str, BlockingFlag],
BeforeValidator(translate_blocked_users)
] = Field(default_factory=dict)

def is_blocked(self, username: str, flag: BlockingFlag) -> bool:
return bool(self.blocked.get(username, BlockingFlag.NONE) & flag)


class InterestsSettings(BaseModel, validate_assignment=True):
Expand Down
1 change: 0 additions & 1 deletion src/aioslsk/shares/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,6 @@ def is_item_locked(self, item: SharedItem, username: str) -> bool:
async def report_shares(self):
"""Reports the shares amount to the server"""
if not self._session:
logger.warning("attempted to report shares without valid session")
return

folder_count, file_count = self.get_stats()
Expand Down
Loading

0 comments on commit a923076

Please sign in to comment.