From 0c67fccb1777ec7169b48fd613234110ffe7ec58 Mon Sep 17 00:00:00 2001 From: amjed alqasemi <59507561+aqasemi@users.noreply.github.com> Date: Thu, 18 Jul 2024 22:25:15 +0000 Subject: [PATCH 01/11] allow for selecting client instance --- goneonize/main.go | 19 ++++++++++++++++--- neonize/client.py | 19 +++++++++++++++++++ neonize/download.py | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/goneonize/main.go b/goneonize/main.go index a64ec6b..93a52d2 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,21 @@ 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 neoJIDProto defproto.JID + + if int(JIDSize) > 0 { + jidbyte := getByteByAddr(JIDByte, JIDSize) + jidbyte_err := proto.Unmarshal(jidbyte, &neoJIDProto) + if jidbyte_err != nil { + panic(jidbyte_err) + } + deviceStore, err_device = container.GetDevice(utils.DecodeJidProto(&neoJIDProto)) + } else { + deviceStore, err_device = container.GetFirstDevice() + } + if err_device != nil { panic(err) } proto.Merge(store.DeviceProps, &deviceProps) diff --git a/neonize/client.py b/neonize/client.py index 1211ab0..6b0006d 100644 --- a/neonize/client.py +++ b/neonize/client.py @@ -344,6 +344,7 @@ class NewClient: def __init__( self, name: str, + jid: Optional[JID] = None, props: Optional[DeviceProps] = None, uuid: Optional[str] = None, ): @@ -351,6 +352,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,6 +362,7 @@ def __init__( """ self.name = name self.device_props = props + self.jid = jid self.uuid = (uuid or name).encode() self.__client = gocode self.event = Event(self) @@ -2435,9 +2438,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 +2505,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), diff --git a/neonize/download.py b/neonize/download.py index c169905..c3b1c5f 100644 --- a/neonize/download.py +++ b/neonize/download.py @@ -33,7 +33,7 @@ def __download(url: str, fname: str, chunk_size=1024): def download(): version = importlib.metadata.version("neonize") __download( - f"https://github.com/krypton-byte/neonize/releases/download/{version}/{generated_name()}", + f"https://github.com/aqasemi/neonize/releases/download/{version}/{generated_name()}", f"{os.path.dirname(__file__)}/{generated_name()}", ) From a29a74cd6241d3e1983f89efc385833754e9a0a7 Mon Sep 17 00:00:00 2001 From: Mjo Date: Sun, 28 Jul 2024 11:27:08 +0300 Subject: [PATCH 02/11] imp: get_all_devices binding --- goneonize/main.go | 35 +++++++++++++++++++++++++++++++---- neonize/_binder.py | 4 ++++ neonize/client.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/goneonize/main.go b/goneonize/main.go index 93a52d2..54e96ce 100644 --- a/goneonize/main.go +++ b/goneonize/main.go @@ -150,15 +150,14 @@ func Neonize(db *C.char, id *C.char, JIDByte *C.uchar, JIDSize C.int, logLevel * // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead. var deviceStore *store.Device var err_device error - var neoJIDProto defproto.JID + var JID defproto.JID if int(JIDSize) > 0 { - jidbyte := getByteByAddr(JIDByte, JIDSize) - jidbyte_err := proto.Unmarshal(jidbyte, &neoJIDProto) + jidbyte_err := proto.Unmarshal(getByteByAddr(JIDByte, JIDSize), &JID) if jidbyte_err != nil { panic(jidbyte_err) } - deviceStore, err_device = container.GetDevice(utils.DecodeJidProto(&neoJIDProto)) + deviceStore, err_device = container.GetDevice(utils.DecodeJidProto(&JID)) } else { deviceStore, err_device = container.GetFirstDevice() } @@ -1941,6 +1940,34 @@ 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 { + result.WriteString("|") + } + 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 6b0006d..f25c0a0 100644 --- a/neonize/client.py +++ b/neonize/client.py @@ -2391,6 +2391,46 @@ def get_newsletter_info(self, jid: JID) -> neonize_proto.NewsletterMetadata: raise GetNewsletterInfoError(model.Error) return model.NewsletterMetadata + @staticmethod + def get_all_devices(db: str) -> List["Device"]: + """ + Retrieves all devices associated with the current account. + + :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, Initialized: bool): + self.JID = JID + self.PushName = PushName + self.BusinessName = BussinessName + self.Initialized = Initialized + + devices: list[Device] = [] + + for device_str in c_string.split('|'): + id, push_name, business_name, initialized = device_str.split(',') + server = id.split('@')[1] if '@' in id else "s.whatsapp.net" + jid = JID( + User=id.split('@')[0], + Device=0, + Integrator=0, + IsEmpty=False, + RawAgent=0, + Server=server, + ) + + device = Device(jid, push_name, business_name, initialized.lower() == 'true') + devices.append(device) + + return devices + + + def PairPhone( self, phone: str, From a0b21c75a74e94a1d2674230e0034cc8ec0ce651 Mon Sep 17 00:00:00 2001 From: Mjo Date: Sun, 28 Jul 2024 11:28:30 +0300 Subject: [PATCH 03/11] revert to original --- neonize/download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neonize/download.py b/neonize/download.py index c3b1c5f..c169905 100644 --- a/neonize/download.py +++ b/neonize/download.py @@ -33,7 +33,7 @@ def __download(url: str, fname: str, chunk_size=1024): def download(): version = importlib.metadata.version("neonize") __download( - f"https://github.com/aqasemi/neonize/releases/download/{version}/{generated_name()}", + f"https://github.com/krypton-byte/neonize/releases/download/{version}/{generated_name()}", f"{os.path.dirname(__file__)}/{generated_name()}", ) From f98144b735f4b30a06c14eeb04fd60b455b8994d Mon Sep 17 00:00:00 2001 From: Mjo Date: Mon, 29 Jul 2024 16:27:28 +0300 Subject: [PATCH 04/11] add events manager --- neonize/events.py | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/neonize/events.py b/neonize/events.py index ccf1bbb..0742a27 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. @@ -189,20 +211,3 @@ def default_blocking(cls, _): event.wait() log.debug("🚦 The function has been unblocked.") - 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: - wrapped_func = self.wrap(func, event) - self.list_func.update({EVENT_TO_INT[event]: wrapped_func}) - - return callback From dc71a7e5507dc768292615239c297eceea9b0ffc Mon Sep 17 00:00:00 2001 From: Mjo Date: Mon, 29 Jul 2024 18:30:31 +0300 Subject: [PATCH 05/11] make an arbitrary delimiter getalldevices --- goneonize/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/goneonize/main.go b/goneonize/main.go index 54e96ce..530c47e 100644 --- a/goneonize/main.go +++ b/goneonize/main.go @@ -159,7 +159,7 @@ func Neonize(db *C.char, id *C.char, JIDByte *C.uchar, JIDSize C.int, logLevel * } deviceStore, err_device = container.GetDevice(utils.DecodeJidProto(&JID)) } else { - deviceStore, err_device = container.GetFirstDevice() + deviceStore, err_device = container.NewDevice(), nil } if err_device != nil { panic(err) @@ -1956,7 +1956,8 @@ func GetAllDevices(db *C.char) *C.char { var result strings.Builder for i, device := range deviceStore { if i > 0 { - result.WriteString("|") + // 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(), From 47208b7644b8141b378e6b0fe63f9cd5b1b86962 Mon Sep 17 00:00:00 2001 From: Mjo Date: Mon, 29 Jul 2024 18:30:47 +0300 Subject: [PATCH 06/11] build_jid add server param --- neonize/utils/jid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, ) From d267510179f10d2a9c079d498bafe053a859e9f5 Mon Sep 17 00:00:00 2001 From: Mjo Date: Mon, 29 Jul 2024 18:31:13 +0300 Subject: [PATCH 07/11] multisession example file --- examples/multisession.py | 306 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 examples/multisession.py diff --git a/examples/multisession.py b/examples/multisession.py new file mode 100644 index 0000000..b0527e1 --- /dev/null +++ b/examples/multisession.py @@ -0,0 +1,306 @@ +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") +for device in client_factory.get_all_devices("mdtest.db"): + client_factory.new_client( + device.JID + ) +# if new_client jid parameter is not passed, it will create a new client + + +@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__": + client_factory.run() From ce1b2369a254cc6c962fc608bc5187ff84147c4a Mon Sep 17 00:00:00 2001 From: Mjo Date: Mon, 29 Jul 2024 18:35:47 +0300 Subject: [PATCH 08/11] implement ClientFactory --- neonize/client.py | 120 +++++++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/neonize/client.py b/neonize/client.py index f25c0a0..5f5a23e 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: @@ -363,7 +364,7 @@ def __init__( self.name = name self.device_props = props self.jid = jid - self.uuid = (uuid or name).encode() + 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 @@ -2391,46 +2392,6 @@ def get_newsletter_info(self, jid: JID) -> neonize_proto.NewsletterMetadata: raise GetNewsletterInfoError(model.Error) return model.NewsletterMetadata - @staticmethod - def get_all_devices(db: str) -> List["Device"]: - """ - Retrieves all devices associated with the current account. - - :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, Initialized: bool): - self.JID = JID - self.PushName = PushName - self.BusinessName = BussinessName - self.Initialized = Initialized - - devices: list[Device] = [] - - for device_str in c_string.split('|'): - id, push_name, business_name, initialized = device_str.split(',') - server = id.split('@')[1] if '@' in id else "s.whatsapp.net" - jid = JID( - User=id.split('@')[0], - Device=0, - Integrator=0, - IsEmpty=False, - RawAgent=0, - Server=server, - ) - - device = Device(jid, push_name, business_name, initialized.lower() == 'true') - devices.append(device) - - return devices - - - def PairPhone( self, phone: str, @@ -2575,3 +2536,76 @@ 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(db: str) -> List["Device"]: + """ + Retrieves all devices associated with the current account. + + :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 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 From 23e89567533498684bab777fd96de965fbe38a6a Mon Sep 17 00:00:00 2001 From: Mjo Date: Mon, 29 Jul 2024 18:44:11 +0300 Subject: [PATCH 09/11] add some doc --- examples/multisession.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/multisession.py b/examples/multisession.py index b0527e1..4bdd644 100644 --- a/examples/multisession.py +++ b/examples/multisession.py @@ -37,12 +37,19 @@ def interrupted(*_): client_factory = ClientFactory("db.sqlite3") -for device in client_factory.get_all_devices("mdtest.db"): + +# create clients from preconfigured sessions +sessions = client_factory.get_all_devices("db.sqlite3") +for device in sessions: client_factory.new_client( device.JID ) # if new_client jid parameter is not passed, it will create a new client +# 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): @@ -303,4 +310,5 @@ def PairStatusMessage(_: NewClient, message: PairStatusEv): if __name__ == "__main__": + # all created clients will be automatically logged in and receive all events client_factory.run() From 0556cc3831c6dd7e44403b590e463eb89ccb4ecf Mon Sep 17 00:00:00 2001 From: Mjo Date: Tue, 30 Jul 2024 07:58:35 +0300 Subject: [PATCH 10/11] fix: revert back to original callable client event --- neonize/events.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/neonize/events.py b/neonize/events.py index 0742a27..1d95019 100644 --- a/neonize/events.py +++ b/neonize/events.py @@ -211,3 +211,20 @@ def default_blocking(cls, _): event.wait() log.debug("🚦 The function has been unblocked.") + 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: + wrapped_func = self.wrap(func, event) + self.list_func.update({EVENT_TO_INT[event]: wrapped_func}) + + return callback \ No newline at end of file From dc9bff9f321a89b954abbc6f033355e8454ad37c Mon Sep 17 00:00:00 2001 From: Mjo Date: Wed, 31 Jul 2024 16:04:39 +0300 Subject: [PATCH 11/11] chore: Refactor get_all_devices method in ClientFactory --- examples/multisession.py | 4 ++-- neonize/client.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/multisession.py b/examples/multisession.py index 4bdd644..9c91547 100644 --- a/examples/multisession.py +++ b/examples/multisession.py @@ -39,14 +39,14 @@ def interrupted(*_): client_factory = ClientFactory("db.sqlite3") # create clients from preconfigured sessions -sessions = client_factory.get_all_devices("db.sqlite3") +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 -# create a new client +# or create a new client # from uuid import uuid4 # client_factory.new_client(uuid=uuid4().hex[:5]) diff --git a/neonize/client.py b/neonize/client.py index 5f5a23e..31909da 100644 --- a/neonize/client.py +++ b/neonize/client.py @@ -2549,10 +2549,10 @@ def __init__(self, database_name: str = 'neonize.db') -> None: self.event = EventsManager(self) @staticmethod - def get_all_devices(db: str) -> List["Device"]: + 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] """ @@ -2579,6 +2579,10 @@ def __init__(self, JID: JID, PushName: str, BussinessName: str = None, Initializ 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.