diff --git a/examples/multisession.py b/examples/multisession.py new file mode 100644 index 0000000..9c91547 --- /dev/null +++ b/examples/multisession.py @@ -0,0 +1,314 @@ +import logging +import os +import signal +import sys +from datetime import timedelta +from neonize.client import ClientFactory, NewClient +from neonize.events import ( + ConnectedEv, + MessageEv, + PairStatusEv, + event, + ReceiptEv, + CallOfferEv, +) + +from neonize.proto.waE2E.WAWebProtobufsE2E_pb2 import ( + Message, + FutureProofMessage, + InteractiveMessage, + MessageContextInfo, + DeviceListMetadata, +) +from neonize.types import MessageServerID +from neonize.utils import log +from neonize.utils.enum import ReceiptType + +sys.path.insert(0, os.getcwd()) + + +def interrupted(*_): + event.set() + + +log.setLevel(logging.DEBUG) +signal.signal(signal.SIGINT, interrupted) + + + +client_factory = ClientFactory("db.sqlite3") + +# create clients from preconfigured sessions +sessions = client_factory.get_all_devices() +for device in sessions: + client_factory.new_client( + device.JID + ) +# if new_client jid parameter is not passed, it will create a new client + +# or create a new client +# from uuid import uuid4 +# client_factory.new_client(uuid=uuid4().hex[:5]) + + +@client_factory.event(ConnectedEv) +def on_connected(_: NewClient, __: ConnectedEv): + log.info("⚡ Connected") + + +@client_factory.event(ReceiptEv) +def on_receipt(_: NewClient, receipt: ReceiptEv): + log.debug(receipt) + + +@client_factory.event(CallOfferEv) +def on_call(_: NewClient, call: CallOfferEv): + log.debug(call) + + +@client_factory.event(MessageEv) +def on_message(client: NewClient, message: MessageEv): + handler(client, message) + + + +def handler(client: NewClient, message: MessageEv): + text = message.Message.conversation or message.Message.extendedTextMessage.text + chat = message.Info.MessageSource.Chat + match text: + case "ping": + client.reply_message("pong", message) + case "_test_link_preview": + client.send_message( + chat, "Test https://github.com/krypton-byte/neonize", link_preview=True + ) + case "_sticker": + client.send_sticker( + chat, + "https://mystickermania.com/cdn/stickers/anime/spy-family-anya-smirk-512x512.png", + ) + case "_sticker_exif": + client.send_sticker( + chat, + "https://mystickermania.com/cdn/stickers/anime/spy-family-anya-smirk-512x512.png", + name="@Neonize", + packname="2024", + ) + case "_image": + client.send_image( + chat, + "https://download.samplelib.com/png/sample-boat-400x300.png", + caption="Test", + quoted=message, + ) + case "_video": + client.send_video( + chat, + "https://download.samplelib.com/mp4/sample-5s.mp4", + caption="Test", + quoted=message, + ) + case "_audio": + client.send_audio( + chat, + "https://download.samplelib.com/mp3/sample-12s.mp3", + quoted=message, + ) + case "_ptt": + client.send_audio( + chat, + "https://download.samplelib.com/mp3/sample-12s.mp3", + ptt=True, + quoted=message, + ) + case "_doc": + client.send_document( + chat, + "https://download.samplelib.com/xls/sample-heavy-1.xls", + caption="Test", + filename="test.xls", + quoted=message, + ) + case "debug": + client.send_message(chat, message.__str__()) + case "viewonce": + client.send_image( + chat, + "https://pbs.twimg.com/media/GC3ywBMb0AAAEWO?format=jpg&name=medium", + viewonce=True, + ) + case "profile_pict": + client.send_message(chat, client.get_profile_picture(chat).__str__()) + case "status_privacy": + client.send_message(chat, client.get_status_privacy().__str__()) + case "read": + client.send_message( + chat, + client.mark_read( + message.Info.ID, + chat=message.Info.MessageSource.Chat, + sender=message.Info.MessageSource.Sender, + receipt=ReceiptType.READ, + ).__str__(), + ) + case "read_channel": + metadata = client.get_newsletter_info_with_invite( + "https://whatsapp.com/channel/0029Va4K0PZ5a245NkngBA2M" + ) + err = client.follow_newsletter(metadata.ID) + client.send_message(chat, "error: " + err.__str__()) + resp = client.newsletter_mark_viewed(metadata.ID, [MessageServerID(0)]) + client.send_message(chat, resp.__str__() + "\n" + metadata.__str__()) + case "logout": + client.logout() + case "send_react_channel": + metadata = client.get_newsletter_info_with_invite( + "https://whatsapp.com/channel/0029Va4K0PZ5a245NkngBA2M" + ) + data_msg = client.get_newsletter_messages( + metadata.ID, 2, MessageServerID(0) + ) + client.send_message(chat, data_msg.__str__()) + for _ in data_msg: + client.newsletter_send_reaction( + metadata.ID, MessageServerID(0), "🗿", "" + ) + case "subscribe_channel_updates": + metadata = client.get_newsletter_info_with_invite( + "https://whatsapp.com/channel/0029Va4K0PZ5a245NkngBA2M" + ) + result = client.newsletter_subscribe_live_updates(metadata.ID) + client.send_message(chat, result.__str__()) + case "mute_channel": + metadata = client.get_newsletter_info_with_invite( + "https://whatsapp.com/channel/0029Va4K0PZ5a245NkngBA2M" + ) + client.send_message( + chat, client.newsletter_toggle_mute(metadata.ID, False).__str__() + ) + case "set_diseapearing": + client.send_message( + chat, client.set_default_disappearing_timer(timedelta(days=7)).__str__() + ) + case "test_contacts": + client.send_message(chat, client.contact.get_all_contacts().__str__()) + case "build_sticker": + client.send_message( + chat, + client.build_sticker_message( + "https://mystickermania.com/cdn/stickers/anime/spy-family-anya-smirk-512x512.png", + message, + "2024", + "neonize", + ), + ) + case "build_video": + client.send_message( + chat, + client.build_video_message( + "https://download.samplelib.com/mp4/sample-5s.mp4", "Test", message + ), + ) + case "build_image": + client.send_message( + chat, + client.build_image_message( + "https://download.samplelib.com/png/sample-boat-400x300.png", + "Test", + message, + ), + ) + case "build_document": + client.send_message( + chat, + client.build_document_message( + "https://download.samplelib.com/xls/sample-heavy-1.xls", + "Test", + "title", + "sample-heavy-1.xls", + quoted=message, + ), + ) + # ChatSettingsStore + case "put_muted_until": + client.chat_settings.put_muted_until(chat, timedelta(seconds=5)) + case "put_pinned_enable": + client.chat_settings.put_pinned(chat, True) + case "put_pinned_disable": + client.chat_settings.put_pinned(chat, False) + case "put_archived_enable": + client.chat_settings.put_archived(chat, True) + case "put_archived_disable": + client.chat_settings.put_archived(chat, False) + case "get_chat_settings": + client.send_message( + chat, client.chat_settings.get_chat_settings(chat).__str__() + ) + case "button": + client.send_message( + message.Info.MessageSource.Chat, + Message( + viewOnceMessage=FutureProofMessage( + message=Message( + messageContextInfo=MessageContextInfo( + deviceListMetadata=DeviceListMetadata(), + deviceListMetadataVersion=2, + ), + interactiveMessage=InteractiveMessage( + body=InteractiveMessage.Body(text="Body Message"), + footer=InteractiveMessage.Footer(text="@krypton-byte"), + header=InteractiveMessage.Header( + title="Title Message", + subtitle="Subtitle Message", + hasMediaAttachment=False, + ), + nativeFlowMessage=InteractiveMessage.NativeFlowMessage( + buttons=[ + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="single_select", + buttonParamsJSON='{"title":"List Buttons","sections":[{"title":"title","highlight_label":"label","rows":[{"header":"header","title":"title","description":"description","id":"select 1"},{"header":"header","title":"title","description":"description","id":"select 2"}]}]}', + ), + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="quick_reply", + buttonParamsJSON='{"display_text":"Quick URL","url":"https://www.google.com","merchant_url":"https://www.google.com"}', + ), + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="cta_call", + buttonParamsJSON='{"display_text":"Quick Call","id":"message"}', + ), + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="cta_copy", + buttonParamsJSON='{"display_text":"Quick Copy","id":"123456789","copy_code":"message"}', + ), + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="cta_remainder", + buttonParamsJSON='{"display_text":"Reminder","id":"message"}', + ), + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="cta_cancel_remainder", + buttonParamsJSON='{"display_text":"Cancel Reminder","id":"message"}', + ), + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="address_message", + buttonParamsJSON='{"display_text":"Address","id":"message"}', + ), + InteractiveMessage.NativeFlowMessage.NativeFlowButton( + name="send_location", buttonParamsJSON="" + ), + ] + ), + ), + ) + ) + ), + ) + + +@client_factory.event(PairStatusEv) +def PairStatusMessage(_: NewClient, message: PairStatusEv): + log.info(f"logged as {message.ID.User}") + + +if __name__ == "__main__": + # all created clients will be automatically logged in and receive all events + client_factory.run() diff --git a/goneonize/main.go b/goneonize/main.go index a64ec6b..530c47e 100644 --- a/goneonize/main.go +++ b/goneonize/main.go @@ -130,7 +130,7 @@ func SendMessage(id *C.char, JIDByte *C.uchar, JIDSize C.int, messageByte *C.uch } //export Neonize -func Neonize(db *C.char, id *C.char, logLevel *C.char, qrCb C.ptr_to_python_function_string, logStatus C.ptr_to_python_function_string, event C.ptr_to_python_function_bytes, subscribes *C.uchar, lenSubscriber C.int, blocking C.ptr_to_python_function, devicePropsBuf *C.uchar, devicePropsSize C.int, pairphone *C.uchar, pairphoneSize C.int) { // , +func Neonize(db *C.char, id *C.char, JIDByte *C.uchar, JIDSize C.int, logLevel *C.char, qrCb C.ptr_to_python_function_string, logStatus C.ptr_to_python_function_string, event C.ptr_to_python_function_bytes, subscribes *C.uchar, lenSubscriber C.int, blocking C.ptr_to_python_function, devicePropsBuf *C.uchar, devicePropsSize C.int, pairphone *C.uchar, pairphoneSize C.int) { // , subscribers := map[int]bool{} var deviceProps waProto.DeviceProps var loginStateChan = make(chan bool) @@ -148,8 +148,20 @@ func Neonize(db *C.char, id *C.char, logLevel *C.char, qrCb C.ptr_to_python_func panic(err) } // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead. - deviceStore, err := container.GetFirstDevice() - if err != nil { + var deviceStore *store.Device + var err_device error + var JID defproto.JID + + if int(JIDSize) > 0 { + jidbyte_err := proto.Unmarshal(getByteByAddr(JIDByte, JIDSize), &JID) + if jidbyte_err != nil { + panic(jidbyte_err) + } + deviceStore, err_device = container.GetDevice(utils.DecodeJidProto(&JID)) + } else { + deviceStore, err_device = container.NewDevice(), nil + } + if err_device != nil { panic(err) } proto.Merge(store.DeviceProps, &deviceProps) @@ -1928,6 +1940,35 @@ func PutArchived(id *C.char, user *C.uchar, userSize C.int, archived C.bool) *C. return C.CString("") } +//export GetAllDevices +func GetAllDevices(db *C.char) *C.char { + dbLog := waLog.Stdout("Database", "ERROR", true) + container, err := sqlstore.New("sqlite3", fmt.Sprintf("file:%s?_foreign_keys=on", C.GoString(db)), dbLog) + if err != nil { + panic(err) + } + + deviceStore, err := container.GetAllDevices() + if err != nil { + panic(err) + } + + var result strings.Builder + for i, device := range deviceStore { + if i > 0 { + // an arbitrary delimiter (a unicode to make sure pushname doesn't collide with it) + result.WriteString("|\u0001|") + } + result.WriteString(fmt.Sprintf("%s,%s,%s,%t", + device.ID.String(), + device.PushName, + device.BusinessName, + device.Initialized)) + } + + return C.CString(result.String()) +} + func main() { } diff --git a/neonize/_binder.py b/neonize/_binder.py index 3bfaadd..e9124fb 100644 --- a/neonize/_binder.py +++ b/neonize/_binder.py @@ -49,6 +49,8 @@ def get_bytes(self): ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, func_string, func_string, func_callback_bytes, @@ -450,5 +452,7 @@ def get_bytes(self): gocode.PutArchived.restype = ctypes.c_char_p gocode.GetChatSettings.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int] gocode.GetChatSettings.restype = Bytes + gocode.GetAllDevices.argtypes = [ctypes.c_char_p] + gocode.GetAllDevices.restype = ctypes.c_char_p else: gocode: Any = object() diff --git a/neonize/client.py b/neonize/client.py index 1211ab0..31909da 100644 --- a/neonize/client.py +++ b/neonize/client.py @@ -15,13 +15,14 @@ from PIL import Image from google.protobuf.internal.containers import RepeatedCompositeFieldContainer from linkpreview import link_preview +from threading import Thread from .utils.calc import AspectRatioMethod, auto_sticker from ._binder import gocode, func_string, func_callback_bytes, func from .builder import build_edit, build_revoke -from .events import Event +from .events import Event, EventsManager from .exc import ( ContactStoreError, DownloadError, @@ -150,7 +151,7 @@ ) from .utils.ffmpeg import FFmpeg, ImageFormat from .utils.iofile import get_bytes_from_name_or_url -from .utils.jid import Jid2String, JIDToNonAD +from .utils.jid import Jid2String, JIDToNonAD, build_jid class ContactStore: @@ -344,6 +345,7 @@ class NewClient: def __init__( self, name: str, + jid: Optional[JID] = None, props: Optional[DeviceProps] = None, uuid: Optional[str] = None, ): @@ -351,6 +353,7 @@ def __init__( :param name: The name or identifier for the new client. :type name: str + :param jid: Optional. The JID (Jabber Identifier) for the client. If not provided, first client is used. :param qrCallback: Optional. A callback function for handling QR code updates, defaults to None. :type qrCallback: Optional[Callable[[NewClient, bytes], None]], optional :param messageCallback: Optional. A callback function for handling incoming messages, defaults to None. @@ -360,7 +363,8 @@ def __init__( """ self.name = name self.device_props = props - self.uuid = (uuid or name).encode() + self.jid = jid + self.uuid = ((jid.User if jid else None) or uuid or name).encode() self.__client = gocode self.event = Event(self) self.blocking = self.event.blocking @@ -2435,9 +2439,17 @@ def PairPhone( else self.device_props ).SerializeToString() + jidbuf_size = 0 + jidbuf = b"" + if self.jid: + jidbuf = self.jid.SerializeToString() + jidbuf_size = len(jidbuf) + self.__client.Neonize( self.name.encode(), self.uuid, + jidbuf, + jidbuf_size, LogLevel.from_logging(log.level).level, func_string(self.__onQr), func_string(self.__onLoginStatus), @@ -2494,10 +2506,18 @@ def connect(self): else self.device_props ).SerializeToString() + jidbuf_size = 0 + jidbuf = b"" + if self.jid: + jidbuf = self.jid.SerializeToString() + jidbuf_size = len(jidbuf) + # Initiate connection to the server self.__client.Neonize( self.name.encode(), self.uuid, + jidbuf, + jidbuf_size, LogLevel.from_logging(log.level).level, func_string(self.__onQr), func_string(self.__onLoginStatus), @@ -2516,3 +2536,80 @@ def disconnect(self) -> None: Disconnect the client """ self.__client.Disconnect(self.uuid) + + + +class ClientFactory: + def __init__(self, database_name: str = 'neonize.db') -> None: + """ + This class is used to create new instances of the client. + """ + self.database_name = database_name + self.clients: list[NewClient] = [] + self.event = EventsManager(self) + + @staticmethod + def get_all_devices_from_db(db: str) -> List["Device"]: + """ + Retrieves all devices associated with the current account. + :param db: The name of the database to retrieve the devices from. + :return: A list of Device-like objects representing all associated devices. + :rtype: List[neonize_proto.Device] + """ + c_string = gocode.GetAllDevices(db.encode()).decode() + if not c_string: + return [] + + class Device: + def __init__(self, JID: JID, PushName: str, BussinessName: str = None, Initialized: bool = None): + self.JID = JID + self.PushName = PushName + self.BusinessName = BussinessName + self.Initialized = Initialized + + devices: list[Device] = [] + + for device_str in c_string.split('|\u0001|'): + id, push_name, bussniess_name, initialized = device_str.split(',') + id, server = id.split('@') + jid = build_jid(id, server) + + device = Device(jid, push_name, bussniess_name, initialized == 'true') + devices.append(device) + + return devices + + def get_all_devices(self) -> List["Device"]: + """Retrieves all devices associated with the current account from the database.""" + return self.get_all_devices_from_db(self.database_name) + + def new_client(self, jid: JID = None, uuid: str = None, props: Optional[DeviceProps] = None) -> NewClient: + """ + This function creates a new instance of the client. If the jid parameter is not provided, a new client will be created. + :param name: The name of the client. + :type name: str + :param uuid: The unique identifier of the client. + :type uuid: str + :param jid: The JID of the client. + :type jid: JID + :param props: The device properties of the client. + :type props: Optional[DeviceProps] + """ + + if not jid and not uuid: + # you must at least provide a uuid to make sure the client is unique + raise Exception("JID and UUID cannot be none") + + client = NewClient(self.database_name, jid, props, uuid) + self.clients.append(client) + return client + + def run(self): + for client in self.clients: + Thread( + target=client.connect, + daemon=True, + name=client.uuid, + ).start() + + Event.default_blocking(None) \ No newline at end of file diff --git a/neonize/events.py b/neonize/events.py index ccf1bbb..1d95019 100644 --- a/neonize/events.py +++ b/neonize/events.py @@ -53,7 +53,7 @@ log = logging.getLogger(__name__) if TYPE_CHECKING: - from .client import NewClient + from .client import NewClient, ClientFactory EventType = TypeVar("EventType", bound=Message) EVENT_TO_INT: Dict[Type[Message], int] = { @@ -101,9 +101,31 @@ event = EventThread() +class EventsManager: + def __init__(self, client_factory: ClientFactory): + self.client_factory = client_factory + + def __call__( + self, event: Type[EventType] + ) -> Callable[[Callable[[NewClient, EventType], None]], None]: + """ + Registers a callback function for a specific event type. + + :param event: The type of event to register the callback for. + :type event: Type[EventType] + :return: A decorator that registers the callback function. + :rtype: Callable[[Callable[[NewClient, EventType], None]], None] + """ + def callback(func: Callable[[NewClient, EventType], None]) -> None: + for client in self.client_factory.clients: + wrapped_func = client.event.wrap(func, event) + client.event.list_func.update({EVENT_TO_INT[event]: wrapped_func}) + + return callback + class Event: - def __init__(self, client): + def __init__(self, client: NewClient): """ Initializes the Event class with a client of type NewClient. Also sets up a default blocking function and an empty dictionary for list functions. @@ -205,4 +227,4 @@ def callback(func: Callable[[NewClient, EventType], None]) -> None: wrapped_func = self.wrap(func, event) self.list_func.update({EVENT_TO_INT[event]: wrapped_func}) - return callback + return callback \ No newline at end of file diff --git a/neonize/utils/jid.py b/neonize/utils/jid.py index 6fdf686..a165d05 100644 --- a/neonize/utils/jid.py +++ b/neonize/utils/jid.py @@ -34,7 +34,7 @@ def Jid2String(jid: JID) -> str: return jid.Server -def build_jid(phone_number: str) -> JID: +def build_jid(phone_number: str, server: str = "s.whatsapp.net") -> JID: """ Builds a JID (Jabber ID) from a phone number. @@ -49,5 +49,5 @@ def build_jid(phone_number: str) -> JID: Integrator=0, IsEmpty=False, RawAgent=0, - Server="s.whatsapp.net", + Server=server, )