mirror of
https://github.com/status-im/status-python-sdk.git
synced 2026-08-30 21:51:14 +00:00
350 lines
14 KiB
Python
350 lines
14 KiB
Python
from .account import Account
|
|
from . import exceptions
|
|
from typing import Union, Optional
|
|
import pandas as pd
|
|
import re, datetime
|
|
|
|
class GroupChat:
|
|
|
|
__TOTAL_MEMBERS = 20 # https://status.app/help/messaging/create-a-group-chat
|
|
def __init__(self, account: Account, chat_id: Optional[str] = None):
|
|
"""
|
|
Work with your own Status App Group Chats.
|
|
|
|
Parameters:
|
|
- `account` - a logged in `Account`
|
|
- `chat_id` - a group chat `id` from `.chats` in `Account`
|
|
"""
|
|
# Verify that the user is logged in
|
|
account.info
|
|
self.__account = account
|
|
self.__id = None
|
|
|
|
if not chat_id:
|
|
return
|
|
|
|
chat = self.__get_group_info(chat_id)
|
|
self.__id = chat["id"]
|
|
is_admin = self.__extract_admin_public_key(chat["id"]) == self.__account.info["public_key"]
|
|
self.__account.logger.info(f"Account is{'' if is_admin else ' NOT'} admin.")
|
|
|
|
def create(self, public_keys: Union[list[str], str], name: str):
|
|
"""
|
|
Create a Group Chat from the given public keys.
|
|
|
|
Parameters:
|
|
- `public_keys` - a single value or a list of public keys / chat keys / account URLs of the members to create the chat with. The formats can be mixed within the same list and can be found in `contacts` in `Account`
|
|
- `name` - the name of the Group Chat. Must follow the Status App naming rules
|
|
|
|
Output:
|
|
- the `GroupChat` itself, so calls can be chained
|
|
"""
|
|
if self.__id:
|
|
raise exceptions.GroupChatAlreadyExistsError("Chat has already been created! To create a new one, please initialize a new `GroupChat`...")
|
|
|
|
self.__validate_name(name)
|
|
public_keys = self.__get_public_keys(public_keys)
|
|
if len(public_keys) > self.__TOTAL_MEMBERS:
|
|
raise exceptions.GroupChatCreationError(f"Group chats can have up to {self.__TOTAL_MEMBERS} members. Please consider creating a Status Community...")
|
|
|
|
response: dict = self.__account._call_rpc("messaging", "createGroupChatWithMembers", [name, public_keys])
|
|
error = response.get("error")
|
|
if error:
|
|
raise exceptions.GroupChatCreationError(error["message"])
|
|
|
|
chat: dict = response["result"]["chats"][0]
|
|
self.__id = chat["id"]
|
|
self.__account.logger.info(f"Created group chat {name} [{self.id}]")
|
|
return self
|
|
|
|
def send_message(self, message: str, reply_to_message_id: Optional[str] = None) -> str:
|
|
"""
|
|
Send a message to the group chat.
|
|
|
|
Parameters:
|
|
- `message` - the message that will be sent. Currently only text messages are supported
|
|
- `reply_to_message_id` - the `id` of the message to reply to, as it appears in `self.get_messages()`. If not provided, the message is sent as a standalone message.
|
|
|
|
Output:
|
|
- The message ID
|
|
"""
|
|
return self.__account.send_message(self.id, message, reply_to_message_id)
|
|
|
|
def send_image(self, file_path: str, message: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> str:
|
|
"""
|
|
Send a image to the group chat.
|
|
|
|
Parameters:
|
|
- `file_path` - the file path of the image
|
|
- `message` - the message that will be sent. Currently only text messages are supported
|
|
- `reply_to_message_id` - the `id` of the message to reply to, as it appears in `self.get_messages()`. If not provided, the message is sent as a standalone message.
|
|
|
|
Output:
|
|
- The message ID
|
|
"""
|
|
return self.__account.send_image(self.id, file_path, message, reply_to_message_id)
|
|
|
|
def send_emoji_reaction(self, message_id: str, emoji_shortname: str):
|
|
"""
|
|
Set / unset emoji reaction for a message in the group chat.
|
|
|
|
Parameters:
|
|
- `message_id` - the `id` of the message, as it appears in `self.get_messages()`
|
|
- `emoji_shortname` - the emoji shortname as in Status App, with or without the surrounding colons
|
|
"""
|
|
self.__account.send_emoji_reaction(message_id, emoji_shortname, self.id)
|
|
|
|
def get_messages(self, start_timestamp: Optional[Union[str, datetime.datetime, datetime.date, pd.Timestamp]] = None, end_timestamp: Optional[Union[str, datetime.datetime, datetime.date, pd.Timestamp]] = None) -> list[dict]:
|
|
"""
|
|
Get all of the messages in the given start and end timestamps.
|
|
Messages are returned in descending order (newest to oldest).
|
|
Messages can be fetched for removed contacts as well.
|
|
|
|
Parameters:
|
|
- `start_timestamp` - the start timestamp for message extraction. If not provided all early messages will be fetched. Can be a `datetime.datetime` or a string like `2026-08-11 22:57:51.134000` / `2026-08-11 22:57` / `2026-08-11`
|
|
- `end_timestamp` - the end timestamp for message extraction. If not provided all latest messages will be fetched. Can be a `datetime.datetime` or a string like `2026-08-11 22:57:51.134000` / `2026-08-11 22:57` / `2026-08-11`
|
|
|
|
Output:
|
|
- All messages within the given range
|
|
"""
|
|
return self.__account.get_messages(self.id, start_timestamp, end_timestamp)
|
|
|
|
def remove(self, public_keys: Union[list[str], str]):
|
|
"""
|
|
Remove members from the Group Chat. Only the administrator of the chat can remove members.
|
|
|
|
Parameters:
|
|
- `public_keys` - a single value or a list of public keys / chat keys / account URLs of the members to remove. The formats can be mixed within the same list and can be found in `self.members`
|
|
|
|
Output:
|
|
- the `GroupChat` itself, so calls can be chained
|
|
"""
|
|
|
|
if len(self.members.keys()) == 0:
|
|
self.__account.logger.error("There are no members to remove from the Group Chat...")
|
|
return
|
|
|
|
if not self.is_admin:
|
|
raise exceptions.GroupChatMembersError("Only administrators can remove members from")
|
|
|
|
public_keys = self.__get_public_keys(public_keys)
|
|
current_members = self.members.keys()
|
|
public_keys = [public_key for public_key in public_keys if public_key in current_members]
|
|
if len(public_keys) == 0:
|
|
raise exceptions.GroupChatMembersError("Please provide valid Public Keys from the chat only...")
|
|
|
|
params = [self.id, public_keys]
|
|
response: dict = self.__account._call_rpc("messaging", "removeMembersFromGroupChat", params)
|
|
self.__action_log(public_keys, "remove")
|
|
return self
|
|
|
|
def add(self, public_keys: Union[list[str], str]):
|
|
"""
|
|
Add members to the Group Chat.
|
|
|
|
Parameters:
|
|
- `public_keys` - a single value or a list of public keys / chat keys / account URLs of the members to add. The formats can be mixed within the same list and can be found in `contacts` in `Account`
|
|
|
|
Output:
|
|
- the `GroupChat` itself, so calls can be chained
|
|
"""
|
|
public_keys = self.__get_public_keys(public_keys)
|
|
current_members = list(self.members.keys())
|
|
public_keys = [
|
|
public_key
|
|
for public_key in public_keys
|
|
if public_key not in current_members
|
|
]
|
|
if len(public_keys) + len(current_members) > self.__TOTAL_MEMBERS:
|
|
self.__account.logger.warning(f"Too many members in the Group Chat! Group chats can have up to {self.__TOTAL_MEMBERS} members. Please consider creating a Status Community...")
|
|
return
|
|
|
|
params = [self.id, public_keys]
|
|
response: dict = self.__account._call_rpc("messaging", "addMembersToGroupChat", params)
|
|
error = response.get("error", {})
|
|
if error:
|
|
raise exceptions.GroupChatMembersError(response["error"]["message"])
|
|
|
|
self.__action_log(public_keys, "add")
|
|
return self
|
|
|
|
def leave(self):
|
|
"""
|
|
Leave the Group Chat. The internal state is cleared afterwards,
|
|
so the `GroupChat` must be re-initialized with a `chat_id`
|
|
(or a new one must be created) before it can be used again.
|
|
|
|
Output:
|
|
- the `GroupChat` itself, so calls can be chained
|
|
"""
|
|
self.__account._call_rpc("messaging", "leaveGroupChat", [self.id, True])
|
|
self.__id = None
|
|
self.__account.logger.info(f"Left group chat {self.name} [{self.id}]")
|
|
return self
|
|
|
|
def delete_message(self, id: str) -> bool:
|
|
"""
|
|
Delete one of your own Group Chat messages.
|
|
|
|
Parameters:
|
|
- `id` - the `id` of the message from `group_chat.get_messages()`.
|
|
|
|
Output:
|
|
- if `True` then the message was deleted. If `False` then the message was not deleted due to permissions.
|
|
"""
|
|
self.name
|
|
return self.__account.delete_message(id)
|
|
|
|
@property
|
|
def members(self) -> dict[str, dict]:
|
|
"""
|
|
The current members in the chat, mapped by their public key.
|
|
"""
|
|
members = {}
|
|
chat = self.__get_group_info(self.id)
|
|
for member in chat["members"]:
|
|
response: dict = self.__account._call_rpc("messaging", "getContactByID", [member["id"]])
|
|
result = response["result"]
|
|
members[member["id"]] = {
|
|
"public_key": result["id"],
|
|
"url": self.__account._call_rpc("urls", "shareUserURLWithData", [result["id"]]).get("result"),
|
|
"display_name": result["displayName"],
|
|
"compressed_key": result["compressedKey"],
|
|
"admin": self.__extract_admin_public_key(self.id) == result["id"]
|
|
}
|
|
return members
|
|
|
|
@property
|
|
def is_admin(self) -> bool:
|
|
"""
|
|
If `True` then the `Account` is the administrator of the group
|
|
"""
|
|
chat = self.__get_group_info(self.id)
|
|
return self.__extract_admin_public_key(chat["id"]) == self.__account.info["public_key"]
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""
|
|
Get the current chat's name
|
|
"""
|
|
chat = self.__get_group_info(self.id)
|
|
return chat["name"]
|
|
|
|
@name.setter
|
|
def name(self, name: str):
|
|
self.__validate_name(name)
|
|
self.__account._call_rpc("messaging", "changeGroupChatName", [self.id, name])
|
|
self.__account.signal.get("envelope.sent")
|
|
|
|
@property
|
|
def id(self) -> str:
|
|
"""
|
|
Get the chat's ID
|
|
"""
|
|
if not self.__id:
|
|
raise exceptions.GroupChatNotFoundError()
|
|
|
|
return self.__id
|
|
|
|
def __extract_admin_public_key(self, chat_id: str) -> str:
|
|
"""
|
|
Extract the Admin's public key from a Group Chat.
|
|
A Group Chat's ID has the administrator's public key appended to it.
|
|
|
|
Parameters:
|
|
- `chat_id` - a valid Group Chat ID
|
|
|
|
Output:
|
|
- the public key of the Group Chat's administrator
|
|
"""
|
|
return chat_id[chat_id.index("0x"):]
|
|
|
|
def __get_public_keys(self, public_keys: Union[list[str], str]) -> list[str]:
|
|
"""
|
|
Convert user input public keys to a list. The `Account`'s own public key is filtered out,
|
|
as the `Account` cannot add or remove itself from a Group Chat.
|
|
|
|
Parameters:
|
|
- `public_keys` - a single value or a list of public keys / chat keys / account URLs. The formats can be mixed within the same list
|
|
|
|
Output:
|
|
- the unique public keys as a list, without the `Account`'s own public key
|
|
"""
|
|
if not isinstance(public_keys, (list, str)):
|
|
pass
|
|
|
|
if isinstance(public_keys, str):
|
|
public_keys = [public_keys]
|
|
|
|
public_keys = [
|
|
self.__account.get_public_key(public_key)
|
|
for public_key in public_keys
|
|
if public_key not in [self.__account.info["public_key"], self.__account.info["compressed_key"], self.__account.info["url"]]
|
|
]
|
|
if len(public_keys) == 0:
|
|
raise exceptions.PublicKeyError("No public keys were given to the method...")
|
|
|
|
return list(set(public_keys))
|
|
|
|
@property
|
|
def available_slots(self) -> bool:
|
|
return self.__TOTAL_MEMBERS - len(self.members)
|
|
|
|
def __validate_name(self, name: str):
|
|
"""
|
|
Validate the Group chat name based on Status App rules.
|
|
|
|
Status App validation rules:
|
|
- Only letters, numbers, underscores (_), periods (.), whitespaces and hyphens (-) allowed
|
|
- Must be between 1 and 30 characters long
|
|
|
|
Parameters:
|
|
- `name` - the group chat name to validate
|
|
|
|
Output:
|
|
- `True` if the name is valid. Otherwise a custom exception is raised
|
|
"""
|
|
|
|
if not isinstance(name, str):
|
|
raise exceptions.InvalidGroupChatNameError("Group chat name must be a string.")
|
|
|
|
if not 1 <= len(name) <= 30 or name == " ":
|
|
raise exceptions.InvalidGroupChatNameError("Group chat name must be between 1 and 30 characters long.")
|
|
|
|
if not re.fullmatch(r"[A-Za-z0-9_. \t-]+", name):
|
|
raise exceptions.InvalidGroupChatNameError("Group chat name can contain only letters, numbers, underscores (_), periods (.), whitespaces and hyphens (-).")
|
|
|
|
def __action_log(self, public_keys: list[str], action: str):
|
|
"""
|
|
Log how many members were affected by an `add` / `remove` action.
|
|
The `action` is converted to its past tense, so `add` is logged as
|
|
`Added` and `remove` is logged as `Removed`.
|
|
|
|
Parameters:
|
|
- `public_keys` - the public keys that the action was performed on
|
|
- `action` - the action that was performed. Either `add` or `remove`
|
|
"""
|
|
past_tense = f"{action}{'d' if action.endswith('e') else 'ed'}".title()
|
|
preposition = "from" if action == "remove" else "to"
|
|
total = len(public_keys)
|
|
self.__account.logger.info(f"{past_tense} {total} contact{'s' if total != 1 else ''} {preposition} '{self.name}'")
|
|
|
|
def __get_group_info(self, chat_id: str) -> dict:
|
|
"""
|
|
Fetch latest group information. Other chat members can also modify some group information.:
|
|
- Added / remove member
|
|
- Change group name
|
|
|
|
Parameters:
|
|
- `chat_id` - the current chat's ID
|
|
|
|
Output:
|
|
- Overall chat information
|
|
"""
|
|
response: dict = self.__account._call_rpc("messaging", "confirmJoiningGroup", [chat_id])
|
|
if response.get("error"):
|
|
raise exceptions.GroupChatNotFoundError(f"Group Chat not found...\nChat ID: {chat_id}")
|
|
|
|
chat: dict = response["result"]["chats"][0]
|
|
return chat
|