# -*- coding: UTF-8 -*-
import random
import websockets
import requests, json, random

from .models import *
from ._package import *
from . import _util, _state
from .logging import Logging
from concurrent.futures import ThreadPoolExecutor
import concurrent.futures
from urllib.parse import urlencode, urlparse
import random
import websockets
import requests, json
import traceback

from .models import *
from ._package import *
from . import _util, _state
from .logging import Logging
from websockets.sync.client import connect
from concurrent.futures import ThreadPoolExecutor
import requests
from io import BytesIO
import io
import time
import datetime
import asyncio, aiohttp, math
import os
import queue
import hashlib
import base64
from typing import Optional
import math
import string
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import textwrap, ssl
import uuid

pool = ThreadPoolExecutor(max_workers=9999)
logger = Logging(theme="catppuccin-mocha", log_text_color="black")

headers = {
	"Accept": "application/json, text/plain, */*",
	"Accept-Encoding": "gzip, deflate, br",
	"content-length": "229",
	"Accept-Language": "en-US",
	"Connection": "keep-alive",
	"Content-Type": "application/x-www-form-urlencoded",
	"Host": "voicecall-wpa.chat.zalo.me",
	"Sec-Ch-Ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\"",
	"Sec-Ch-Ua-Mobile": "?0",
	"Sec-Ch-Ua-Platform": "\"Windows\"",
	"Sec-Fetch-Dest": "empty",
	"Sec-Fetch-Mode": "cors",
	"Sec-Fetch-Site": "cross-site",
	"User-Agent": "ZaloPC-win32-24v646"
}
HEADERS = {
	"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
	"Accept": "application/json, text/plain, */*",
	"sec-ch-ua": "\"Not-A.Brand\";v=\"99\", \"Chromium\";v=\"124\"",
	"sec-ch-ua-mobile": "?0",
	"sec-ch-ua-platform": "\"Linux\"",
	"origin": "https://chat.zalo.me",
	"sec-fetch-site": "same-site",
	"sec-fetch-mode": "cors",
	"sec-fetch-dest": "empty",
	"Accept-Encoding": "gzip",
	"referer": "https://chat.zalo.me/",
	"accept-language": "vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5",
}

async def check_server_checksum(url: str, headers) -> Optional[str]:
	async with aiohttp.ClientSession() as session:
		async with session.get(url, headers=headers) as resp:
			if 'Content-MD5' in resp.headers:
				md5_base64 = resp.headers['Content-MD5']
				md5_hex = base64.b64decode(md5_base64).hex()
				return md5_hex
	return None

async def check_range_support_and_get_length(url: str, headers) -> tuple[bool, int]:
	async with aiohttp.ClientSession() as session:
		async with session.get(url, headers=headers) as resp:
			if resp.status != 200:
				raise ValueError(f"HEAD failed: {resp.status}")
			accept_ranges = resp.headers.get('Accept-Ranges', '')
			content_length = int(resp.headers.get('Content-Length', 0))
			supports_range = 'bytes' in accept_ranges
			return supports_range, content_length

async def stream_download_and_hash(url: str, headers, chunk_size: int = 10 * 1024 * 1024) -> str:
	md5_hash = hashlib.md5()
	async with aiohttp.ClientSession() as session:
		async with session.get(url, headers=headers) as resp:
			if resp.status != 200:
				raise ValueError(f"GET failed: {resp.status}")
			while True:
				chunk = await resp.content.read(chunk_size)
				if not chunk:
					break
				md5_hash.update(chunk)
	return md5_hash.hexdigest()

async def fetch_range(session: aiohttp.ClientSession, url: str, start: int, end: int, queue: asyncio.Queue, index: int, small_chunk_size: int, sem: asyncio.Semaphore, headers):
	async with sem:
		headers = {'Range': f'bytes={start}-{end}'}
		async with session.get(url, headers=headers) as resp:
			if resp.status != 206:
				raise ValueError(f"Range failed: {resp.status}")
			sub_index = 0
			while True:
				chunk = await resp.content.read(small_chunk_size)
				await queue.put((index, sub_index, chunk))
				sub_index += 1
				if not chunk:
					break
			await queue.put((index, -1, None))

async def parallel_download_and_hash(url: str, headers, max_part_size_mb: int = 50, max_concurrent: int = 8, small_chunk_size: int = 10 * 1024 * 1024, max_num_parts: int = 100) -> str:
	supports_range, content_length = await check_range_support_and_get_length(url, headers)
	if not supports_range or content_length == 0:
		# print("Không hỗ trợ range, fallback stream.")
		return await stream_download_and_hash(url, headers)
	
	
	max_part_size = max_part_size_mb * 1024 * 1024
	num_parts = math.ceil(content_length / max_part_size)
	# print(f"Hỗ trợ range, tải parallel với {num_parts} parts (mỗi part < {max_part_size_mb} MB, concurrent max {max_concurrent}).")
	part_size = content_length // num_parts
	parts = []
	for i in range(num_parts):
		start = i * part_size
		end = start + part_size - 1 if i < num_parts - 1 else content_length - 1
		parts.append((start, end))
	
	md5_hash = hashlib.md5()
	queue = asyncio.Queue()
	sem = asyncio.Semaphore(max_concurrent)
	async with aiohttp.ClientSession() as session:
		tasks = [asyncio.create_task(fetch_range(session, url, start, end, queue, i, small_chunk_size, sem, headers)) for i, (start, end) in enumerate(parts)]
		next_part = 0
		part_buffers = {i: {'chunks': [], 'ended': False} for i in range(num_parts)}
		while next_part < num_parts:
			if part_buffers[next_part]['ended']:
				for chunk in part_buffers[next_part]['chunks']:
					md5_hash.update(chunk)
				del part_buffers[next_part]
				next_part += 1
			else:
				item = await queue.get()
				part, sub, chunk = item
				if sub == -1:
					part_buffers[part]['ended'] = True
				else:
					part_buffers[part]['chunks'].append(chunk)
				queue.task_done()
		
		await asyncio.gather(*tasks, return_exceptions=True)
	
	return md5_hash.hexdigest()

async def getMd5FileUrl(url, headers=None):
	import time
	start_time = time.time()
	server_md5 = await check_server_checksum(url, headers)
	if server_md5:
		return server_md5
	else:
		md5 = await parallel_download_and_hash(url, headers)
		return md5
		
class ZaloAPI(object):
	def __init__(self, phone, password, imei, session_cookies=None, user_agent=None, auto_login=True, prefix=".."):
		"""Initialize and log in the client.

		Args:
			imei (str): The device imei is logged into Zalo
			phone (str): Zalo account phone number
			password (str): Zalo account password
			auto_login (bool): Automatically log in when initializing ZaloAPI (Default: True)
			user_agent (str): Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list
			session_cookies (dict): Cookies from a previous session (Required if logging in with cookies)

		Raises:
			ZaloLoginError: On failed login
			LoginMethodNotSupport: If method login not support
		"""
		self.phone = phone
		self.password = password
		self.session_cookies = session_cookies
		self.prefix = prefix
		self.fileEvent = queue.Queue()
		self.getMsgEvent = queue.Queue()
		self.request_id = 0
		self.ttl = 0
		# self.conversation_handlers = {}
		self.convers_handlers = {}
		self._state = _state.State()
		self._condition = threading.Event()
		self._listening = False
		self._start_fix = False


		if auto_login:
			if (
				not session_cookies 
				or not self.setSession(session_cookies) 
				or not self.isLoggedIn()
			):
				self.login(phone, password, imei, user_agent)


	def uid(self):
		"""The ID of the client."""
		return self.uid
	
	def registerNextStep(self, func, author_id, msgId, *args, **kwargs):
		if not author_id:
			print("Could not find message sender id, function not registered!")
			return
		else:
			self.convers_handlers[author_id] = {"handler": func, "lastMsgId": msgId, "args": args, "kwargs": kwargs}
			
	"""
	INTERNAL REQUEST METHODS
	"""

	def _get(self, *args, **kwargs):
		return self._state._get(*args, **kwargs)

	def _post(self, *args, **kwargs):
		return self._state._post(*args, **kwargs)

	"""
	END INTERNAL REQUEST METHODS
	"""

	"""
	EXTENSIONS METHODS
	"""

	def _encode(self, params, key=None):
		if key == None:
			key = self._state._config.get("secret_key")
		else: 
			key = key
		return _util.zalo_encode(params, key)

	def _decode(self, params, key=None):
		if key == None:
			key = self._state._config.get("secret_key")
		else: 
			key = key
		return _util.zalo_decode(params, key) 

	"""
	END EXTENSIONS METHODS
	"""

	"""
	LOGIN METHODS
	"""

	def isLoggedIn(self):
		"""Get data from config to check the login status.

		Returns:
			bool: True if the client is still logged in
		"""
		return self._state.is_logged_in()

	def getSession(self):
		"""Retrieve session cookies.

		Returns:
			dict: A dictionary containing session cookies
		"""
		return self._state.get_cookies()

	def setSession(self, session_cookies):
		"""Load session cookies.

		Warning:
			Error sending requests if session cookie is wrong

		Args:
			session_cookies (dict): A dictionary containing session cookies

		Returns:
			Bool: False if ``session_cookies`` does not contain proper cookies
		"""
		try:
			if not isinstance(session_cookies, dict):
				return False
			# Load cookies into current session
			self._state.set_cookies(session_cookies)
			self.uid = self._state.user_id
		except Exception as e:
			print("Failed loading session")
			return False
		return True

	def getSecretKey(self):
		"""Retrieve secret key to encode/decode payload.

		Returns:
			str: A secret key string with base64 encode
		"""
		return self._state.get_secret_key()

	def setSecretKey(self, secret_key):
		"""Load secret key.

		Warning:
			Error (enc/de)code payload if secret key is wrong

		Args:
			secret_key (str): A secret key string with base64 encode

		Returns:
			bool: False if ``secret_key`` not correct to (en/de)code the payload
		"""
		try:
			self._state.set_secret_key(secret_key)

			return True
		except:
			return False

	def login(self, phone, password, imei, user_agent=None):
		"""Login the user, using ``phone`` and ``password``.

		If the user is already logged in, this will do a re-login.

		Args:
			imei (str): The device imei is logged into Zalo
			phone (str): Zalo account phone number
			password (str): Zalo account password
			user_agent (str): Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list

		Raises:
			ZaloLoginError: On failed login
			LoginMethodNotSupport: If method login not support
		"""
		if not (phone and password):
			raise ZaloUserError("Phone and password not set")

		# self.onLoggingIn()

		self._state.login(phone, password, imei, user_agent=user_agent)
		try:
			self._imei = imei
			self.uid = self.fetchAccountInfo().profile.get("userId", self._state.user_id)
		except:
			self._imei = imei
			self.uid = self._state.user_id

		self.onLoggedIn(self._state._config.get("phone_number"))

	"""
	END LOGIN METHODS
	"""

	"""
	ATTACHMENTS METHODS
	"""

	def _uploadImage(self, imagePath, thread_id, thread_type, isE2EE=0, jxl=0):
		"""Upload images to Zalo.

		Args:
			filePath (str): Image path to upload
			thread_id (int | str): User/Group ID to upload to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			dict: A dictionary containing the image info just uploaded
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		files = [("chunkContent", open(imagePath, "rb"))]
		fileSize = len(open(imagePath, "rb").read())
		fileName = imagePath if "/" not in imagePath else imagePath.rstrip("/")[1]

		params = {
			"params": {
				"totalChunk": 1,
				"fileName": fileName,
				"clientId": _util.now(),
				"totalSize": fileSize,
				"imei": self._imei,
				"isE2EE": isE2EE,
				"jxl": jxl,
				"chunkId": 1
			},
			"zpw_ver": 655,
			"zpw_type": 24,
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/photo_original/upload"
			params["type"] = 2
			params["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/photo_original/upload"
			params["type"] = 11
			params["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		params["params"] = self._encode(params["params"])

		response = self._post(url, params=params, files=files)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(data["data"])
			# print(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def setPassWord(self, pin='5f268dfb0fbef44de0f668a022707b86'):
		"""Set pin hidden message

		Args:
			pin (int | str): pass 5f268dfb0fbef44de0f668a022707b86 = 3006

		Returns:
			object: `Group` client info
			dict: A dictionary containing error_code, response if failed

			Raises:
				ZaloAPIException: If request failed

		"""
		params = {
			"params": self._encode({
				'new_pin': str(pin), # 3006
				'imei': self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-convers-wpa.chat.zalo.me/api/hiddenconvers/update-pin", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			# print(results)
			results = results.get("error_message") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}
			results = {"error_code": 0, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")


	"""
	END ATTACHMENTS METHODS
	"""

	"""
	FETCH METHODS
	"""

	def fetchLinkBox(self, url, mpage=1, max_pages=100):
		"""Fetch account information of the client 

		Args:
			url (str): link box to fetch data
			mpage (str | int): mpage

		Returns:
			object: `Group` client info
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		mpage = int(mpage)
		currentMems = []
		prev_len = 0

		while mpage <= max_pages: 
			params = {
				"params": self._encode({
					'link': str(url), 
					'avatar_size': 120, 
					'member_avatar_size': 120, 
					'mpage': mpage
				}),
				"zpw_ver": 655,
				"zpw_type": 24
			}

			response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/link/ginfo", params=params)
			data = response.json()

			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("error_code") == 0 else results
				if results is None:
					return {"error_code": 1337, "error_message": "Data is None", "currentMems": currentMems}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						return {"error_code": 1337, "error_message": results, "currentMems": currentMems}

				new_mems = results.get('currentMems', [])
				if not new_mems:
					break

				currentMems.extend(new_mems)

				if int(results.get('totalMember')) == len(currentMems):
					break
				if len(currentMems) == prev_len:
					break
				prev_len = len(currentMems)
				mpage += 1

		results["currentMems"] = currentMems  
		return Group.fromDict(results, None)


	def fetchAccountInfo(self):
		"""fetch account information of the client 

		Returns:
			object: `User` client info
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"params": self._encode({
				"avatar_size": 120,
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24,
			"os": 8,
			"browser": 0
		}

		response = self._get("https://tt-profile-wpa.chat.zalo.me/api/social/profile/me-v2", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def fetchPhoneNumber(self, phoneNumber, language="vi"):
		"""Fetch user info by Phone Number.

		Not available with hidden phone numbers

		Args:
			phoneNumber (int | str): Phone number to fetch information
			language (str): Language for response (not sure | Default: vi)

		Returns:
			object: `User` user(s) info
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""

		phone = "84" + str(phoneNumber) if str(phoneNumber)[:1] != "0" else "84" + str(phoneNumber)[1:]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"phone": phone,
				"avatar_size": 240,
				"language": language,
				"imei": self._imei,
				"reqSrc": 32
			})
		}

		response = self._get("https://tt-friend-wpa.chat.zalo.me/api/friend/profile/get", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def fetchUserName(self, userName, language="vi"):
		"""Fetch user info by User Name.

		Not available with hidden phone numbers

		Args:
			phoneNumber (int | str): Phone number to fetch information
			language (str): Language for response (not sure | Default: vi)

		Returns:
			object: `User` user(s) info
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""

		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"user_name": userName,
				"avatar_size": 240
			})
		}

		response = self._get("https://tt-friend-wpa.chat.zalo.me/api/friend/search/by-user-name", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def fetchUserInfo(self, userId):
		"""Fetch user info by ID.

		Args:
			userId (int | str | list): User(s) ID to get info

		Returns:
			object: `User` user(s) info
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"phonebook_version": int(_util.now() / 1000),
				"friend_pversion_map": [],
				"avatar_size": 1200,
				"language": "vi",
				"show_online_status": 1,
				"imei": self._imei
			}
		}

		if isinstance(userId, list):
			for i in range(len(userId)):
				userId[i] = str(userId[i]) + "_0"
			payload["params"]["friend_pversion_map"] = userId

		else:
			payload["params"]["friend_pversion_map"].append(str(userId) + "_0")

		payload["params"] = self._encode(payload["params"])

		response = self._post("https://tt-profile-wpa.chat.zalo.me/api/social/friend/getprofiles/v2", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def fetchAvatarUser(self, userId):
		"""Fetch avatar user ( siu nét 100% )

		Args:
			userId (int | str): User ID to get avatar

		Returns
			bk_full_avatar
			full_avatar
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'fid': userId, 
				'imei': self._imei
			})
		}
		response = self._get("https://tt-profile-wpa.chat.zalo.me/api/social/profile/avatar", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")


	def fetchGroupInfo(self, groupId):
		"""Fetch group info by ID.

		Args:
			groupId (int | str | dict): Group(s) ID to get info

		Returns:
			object: `Group` group info
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"gridVerMap": {}
			}
		}

		if isinstance(groupId, dict):
			for i in groupId:
				payload["params"]["gridVerMap"][str(i)] = 0
		else:
			payload["params"]["gridVerMap"][str(groupId)] = 0

		payload["params"]["gridVerMap"] = json.dumps(payload["params"]["gridVerMap"])
		payload["params"] = self._encode(payload["params"])

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/getmg-v2", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def fetchAllFriends(self):
		"""Fetch all users the client is currently chatting with (only friends).

		Returns:
			object: `User` all friend IDs
			any: If response is not list friends

		Raises:
			ZaloAPIException: If request failed
		"""

		params = {
			"params": self._encode({
				"incInvalid": 0,
				"page": 1,
				"count": 20000,
				"avatar_size": 120,
				"actiontime": 0
			}),
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		response = self._get("https://profile-wpa.chat.zalo.me/api/social/friend/getfriends", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def fetchAllGroups(self):
		"""Fetch all group IDs are joining and chatting.

		Returns:
			object: `Group` all group IDs
			any: If response is not all group IDs

		Raises:
			ZaloAPIException: If request failed
		"""

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/getlg/v4", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")


	def fetchLinkGroup(self, thread_id):
		"""Fetch all friend requests

		Returns:
			object: `User` all group IDs
			any: If response is not all group IDs

		Raises:
			ZaloAPIException: If request failed
		"""

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'grid': thread_id, 
				'imei': self._imei
			})
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/link/detail", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def autoReplyCreate(self, content):
		"""auto reply message setting ( Features on business )

		Returns:
			object: `User` all group IDs
			any: If response is not all group IDs

		Raises:
			ZaloAPIException: If request failed
		"""
		# https://auto-rep-msg.chat.zalo.me/api/autoreply/create?zpw_ver=657&zpw_type=24&params=AiMO1KCFBE26SbkDeM7yM5%2Bw%2FgBlgnAu1p1un9npm%2FI1v401uoC0y1Uja5mqZZ6TO3j9QAAXquvrRAEZmgq1CWX%2FvBiXcZ4tCiIVkMM3XTUyUh%2B2Si6TFRzJ3aoQsCUJZYTPOdNLsl1d2HdXpRFiF1gzsOldl6pzG4%2BCABnzJgIqgQLd574Qyn5GLCjF7MOF84Q6KPVA2OLfvyc1V%2BJrHIPwf9X3tTecFMhWA2wQlHri89NJ99kAmjwIW1I1drrm
		params = {
			"zpw_ver": 657,
			"zpw_type": 24,
			"params": self._encode({
				'cliLang': 'vi', 
				'enable': True, 
				'content': content, 
				'startTime': self.timeDay(), 
				'endTime': self.timeDay() + 86400000, 
				'recurrence': ['RRULE:FREQ=DAILY;'], 
				'scope': 1, 
				'uids': []
			})
		}
	
		response = self._get("https://auto-rep-msg.chat.zalo.me/api/autoreply/create", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}
	
			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}
	
			return Group.fromDict(results, None)
	
		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def fetchSticker(self, sticker_id):
		"""Fetch sticker with sticker id

		Returns:
			object: `User` all group IDs
			any: If response is not all group IDs

		Raises:
			ZaloAPIException: If request failed
		"""

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'cid': str(sticker_id)
			})
		}

		response = self._get("https://tt-sticker-wpa.chat.zalo.me/api/message/sticker/category/sticker_detail", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
	
	
	def fetchFriendReq(self):
		"""Fetch all friend requests

		Returns:
			object: `User` all group IDs
			any: If response is not all group IDs

		Raises:
			ZaloAPIException: If request failed
		"""

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"imei": self._imei
			})
		}

		response = self._get("https://tt-friend-wpa.chat.zalo.me/api/friend/requested/list", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")


	"""
	END FETCH METHODS
	"""

	"""
	GET METHODS
	"""

	def reqBackupDataMessageInZaloPC(self, pc_name="Tahn Handsome Vl", public_key=None):
		if public_key == None:
			private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
			public_key = private_key.public_key()
			pem = public_key.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)
			pem_str = pem.decode('utf-8').strip()
			public_key = ''.join(pem_str.splitlines()[1:-1])
			# print(pem_oneline)
		params = {
			"zpw_ver": 655,
			"zpw_type": 30,
			"params": self._encode({
				'pc_name': pc_name, 
				'public_key': public_key, 
				'from_seq_id': _util.now() + 100000000, 
				'is_retry': 1, 
				'min_seq_id': _util.now(), 
				'temp_key': '', 
				'imei': self._imei
			}),
			"nretry": 0
		}
		response = self._get("https://tt-files-wpa.chat.zalo.me/api/message/pull_mobile_msg", params=params)
		# print(response.content)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			return User.fromDict(results, None)
			
	def reqBackupDataMessageInZaloPC_POST(self, pc_name="Tahn Handsome Vl", public_key=None):
		if public_key == None:
			private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
			public_key = private_key.public_key()
			pem = public_key.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)
			pem_str = pem.decode('utf-8').strip()
			public_key = ''.join(pem_str.splitlines()[1:-1])
			# print(pem_oneline)
		params = {
			"zpw_ver": 655,
			"zpw_type": 30,
			"nretry": 0
		}
		payload = {
			"params": self._encode({
				'pc_name': pc_name, 
				'public_key': public_key, 
				'from_seq_id': _util.now() + 100000000, 
				'is_retry': 1, 
				'min_seq_id': _util.now(), 
				'temp_key': '', 
				'imei': self._imei
			})
		}
		response = self._post("https://tt-files-wpa.chat.zalo.me/api/message/pull_mobile_msg", params=params, data=payload)
		# print(response.content)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			return User.fromDict(results, None)
		# except:
			# traceback.print_exc()
	def getGlobalNoiseId(self, userIds):
		# get global id not real user id with userId
		# userIds list type
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"noiseUids": userIds
			})
		}
		response = self._get("https://tt-profile-wpa.chat.zalo.me/api/gid/encrypt", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def getUidFromGlobalNoiseId(self, globalUids):
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"globalUids": globalUids
			})
		}
		response = self._get("https://tt-profile-wpa.chat.zalo.me/api/gid/decrypt", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
	
	def updateStatus(self, status):
		#idk
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}
		payload = {
			"params": self._encode({
				"status": status
			})
		}
		response = self._post("https://tt-profile-wpa.chat.zalo.me/api/social/profile/status", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			print(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
	
		
	def keepAlive(self):
		# dont work
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"imei": self._imei
			})
		}
		response = self._get("https://tt-chat1-wpa.chat.zalo.me/keepalive", params=params)
		data = response.json()
		return User.fromDict(data, None)

	
	def disableLinkGroup(self, groupId):
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"grid": str(groupId)
			})
		}
		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/link/disable", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
	
	def renewLinkGroup(self, groupId):
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"grid": str(groupId), 
				"imei": self._imei
			})
		}
		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/link/new", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def getLastMsgs(self, msgId=None):
		"""Get last message the client's friends/group chat room.

		Returns:
			object: `User` last msg data
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": "655",
			"zpw_type": "24",
			"params": self._encode({
				"threadIdLocalMsgId": msgId if msgId else json.dumps({}),
				"imei": self._imei
			})
		}
		response = self._get("https://tt-convers-wpa.chat.zalo.me/api/preloadconvers/get-last-msgs", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def getRecentGroup(self, groupId, msgId=10000000000000000, count=50, src=6):
		"""Get recent messages in group by ID.

		Args:
			groupId (int | str): Group ID to get recent msgs
			msgId (int | str): Msg last you want to get

		Returns:
			object: `Group` List msg data in groupMsgs
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"params": self._encode({
				"groupId": str(groupId),
				"globalMsgId": msgId,
				"count": count,
				"msgIds": [str(msgId)],
				"imei": self._imei,
				"src": src
			}),
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0,
		}

		response = self._get("https://tt-group-cm.chat.zalo.me/api/cm/getrecentv2", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = json.loads(results.get("data")) if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def _getGroupBoardList(self, board_type, page, count, last_id, last_type, groupId):
		params = {
			"params": self._encode({
				"group_id": str(groupId),
				"board_type": board_type,
				"page": page,
				"count": count,
				"last_id": last_id,
				"last_type": last_type,
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://groupboard-wpa.chat.zalo.me/api/board/list", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = Group.fromDict(results.get("data"), None)

			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def getGroupBoardList(self, groupId, page=1, count=20, last_id=0, last_type=0):
		"""Get group board list (pinmsg, note, poll) by ID.

		Args:
			groupId (int | str): Group ID to get board list
			page (int): Number of pages to retrieve data from
			count (int): Amount of data to retrieve per page (5 poll, ..)
			last_id (int): Default (no description)
			last_type (int): Default (no description)

		Returns:
			object: `Group` board data in group
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		response = self._getGroupBoardList(0, page, count, last_id, last_type, groupId)

		return response

	def getGroupPinMsg(self, groupId, page=1, count=20, last_id=0, last_type=0):
		"""Get group pinned messages by ID.

		Args:
			groupId (int | str): Group ID to get pinned messages
			page (int): Number of pages to retrieve data from
			count (int): Amount of data to retrieve per page (5 message, ..)
			last_id (int): Default (no description)
			last_type (int): Default (no description)

		Returns:
			object: `Group` pinned messages in group
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		response = self._getGroupBoardList(2, page, count, last_id, last_type, groupId)

		return response

	def getGroupNote(self, groupId, page=1, count=20, last_id=0, last_type=0):
		"""Get group notes by ID.

		Args:
			groupId (int | str): Group ID to get notes
			page (int): Number of pages to retrieve data from
			count (int): Amount of data to retrieve per page (5 notes, ..)
			last_id (int): Default (no description)
			last_type (int): Default (no description)

		Returns:
			object: `Group` notes in group
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		response = self._getGroupBoardList(1, page, count, last_id, last_type, groupId)

		return response

	def getGroupPoll(self, groupId, page=1, count=20, last_id=0, last_type=0):
		"""Get group polls by ID.

		Args:
			groupId (int | str): Group ID to get polls
			page (int): Number of pages to retrieve data from
			count (int): Amount of data to retrieve per page (5 poll, ..)
			last_id (int): Default (no description)
			last_type (int): Default (no description)

		Returns:
			object: `Group` polls in group
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		response = self._getGroupBoardList(3, page, count, last_id, last_type, groupId)

		return response

	"""
	END GET METHODS
	"""

	"""
	ACCOUNT ACTION METHODS
	"""

	def changeAccountSetting(self, name, dob, gender, biz={}, language="vi"):
		"""Change account information.

		Args:
			name (str): The new account name
			dob (str): Date of birth wants to change (format: year-month-day)
			gender (int | str): Gender wants to change (0 = Male, 1 = Female)
			biz (unknown): idk this
			language (str): Zalo language wants to change (default = vn)

		Returns:
			object: `User` change account setting status
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"profile": json.dumps({
					"name": name,
					"dob": dob,
					"gender": int(gender),
					# "uname": "tahn"
				}),
				"biz": json.dumps(biz),
				"language": language
			})
		}

		response = self._post("https://tt-profile-wpa.chat.zalo.me/api/social/profile/update", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def changeAccountAvatar(self, filePath, width=500, height=500, language="vn", size=None):
		"""Upload/Change account avatar.

		Args:
			filePath (str): A path to the image to upload/change avatar
			size (int): Avatar image size (default = auto)
			width (int): Width of avatar image
			height (int): height of avatar image
			language (int | str): Zalo Website language ? (idk)

		Returns:
			object: `User` Account avatar change status
			None: If requet success/failed depending on the case
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if not os.path.exists(filePath):
			raise ZaloUserError(f"{filePath} not found")

		size = os.stat(filePath).st_size if not size else size
		files = [("fileContent", open(filePath, "rb"))]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"avatarSize": 120,
				"clientId": str(self.uid) + _util.formatTime("%H:%M %d/%m/%Y"),
				"language": language,
				"metaData": json.dumps({
					"origin": {
						"width": width,
						"height": height
					},
					"processed": {
						"width": width,
						"height": height,
						"size": size
					}
				})
			})
		}

		response = self._post("https://tt-files-wpa.chat.zalo.me/api/profile/upavatar", params=params, files=files)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	"""
	END ACCOUNT ACTION METHODS
	"""

	"""
	USER ACTION METHODS
	"""

	def sendFriendRequest(self, userId, msg, language="vi"):
		"""Send friend request to a user by ID.

		Args:
			userId (int | str): User ID to send friend request
			msg (str): Friend request message
			language (str): Response language or Zalo interface language

		Returns:
			object: `User` Friend requet response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 664,
			"zpw_type": 30
		}

		payload = {
			"params": self._encode({
				"toid": str(userId),
				"msg": msg,
				"reqsrc": 30,
				"imei": self._imei,
				"language": language,
				"srcParams": json.dumps({
					"uidTo": str(userId)
				})
			})
		}

		response = self._post("https://tt-friend-wpa.chat.zalo.me/api/friend/sendreq", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def acceptFriendRequest(self, userId, language="vi"):
		"""Accept friend request from user by ID.

		Args:
			userId (int | str): User ID to accept friend request
			language (str): Response language or Zalo interface language

		Returns:
			object: `User` Friend accept requet response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"fid": str(userId),
				"language": language
			})
		}

		response = self._post("https://tt-friend-wpa.chat.zalo.me/api/friend/accept", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def blockViewFeed(self, userId, isBlockFeed):
		"""Block/Unblock friend view feed by ID.

		Args:
			userId (int | str): User ID to block/unblock view feed
			isBlockFeed (int): Block/Unblock friend view feed (1 = True | 0 = False)

		Returns:
			object: `User` Friend requet response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"fid": str(userId),
				"isBlockFeed": isBlockFeed,
				"imei": self._imei
			})
		}

		response = self._post("https://tt-friend-wpa.chat.zalo.me/api/friend/feed/block", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def blockUser(self, userId):
		"""Block user by ID.

		Args:
			userId (int | str): User ID to block

		Returns:
			object: `User` Block response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"fid": str(userId),
				"imei": self._imei
			})
		}

		response = self._post("https://tt-friend-wpa.chat.zalo.me/api/friend/block", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def unblockUser(self, userId):
		"""Unblock user by ID.

		Args:
			userId (int | str): User ID to unblock

		Returns:
			object: `User` Unblock response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"fid": str(userId),
				"imei": self._imei
			})
		}

		response = self._post("https://tt-friend-wpa.chat.zalo.me/api/friend/unblock", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def infoLinkBox(self, link):
		"""Info Group By Link

		Args:
			link (str): Link to Info

		Returns:
			object: `User` Unblock response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'link': str(link), 
				'version': 1, 
				'imei': self._imei
			})
		}

		response = self._get("https://tt-files-wpa.chat.zalo.me/api/message/parselink", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def infoLinkBoxv2(self, link):
		"""Info Group By Link

		Args:
			link (str): Link to Info

		Returns:
			object: `User` Unblock response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'link': str(link), 
				'avatar_size': 120, 
				'member_avatar_size': 120, 
				'mpage': 1
			})
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/link/ginfo", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return User.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def todo(self, userId, content, thread_id, thread_type):
		"""Assign todo user by ID or Group ID

			Args:
				userId (int | str): User ID to todo
				content (str): Content to assign work
				thread_id (int | str): User/Group ID to send to.
				thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			Idk

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(userId, list):
			userId = [str(userid) for userid in userId]
		else:
			userId = [str(userId)]

		if thread_type == ThreadType.USER:
			params = {
				"params": self._encode({
					'assignees': userId, 
					'dueDate': -1, 
					'content': str(content), 
					'description': '', 
					'extra': json.dumps({
						"toUid": str(thread_id),
						"isGroup": False
					}), 
					'dateDefaultType': 0, 
					'status': -1, 
					'src': 5, 
					'imei': self._imei
				}),
				"zpw_ver": 655,
				"zpw_type": 24
			}

			response = self._post("https://board-wpa.chat.zalo.me/api/board/personal/todo/create", params=params)
			data = response.json()
			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("data") else results
				if results == None:
					results = {"error_code": 1337, "error_message": "Data is None"}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						results = {"error_code": 1337, "error_message": results}

				return Group.fromDict(results, None)

			error_code = data.get("error_code")
			error_message = data.get("error_message") or data.get("data")
			raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

		elif thread_type == ThreadType.GROUP:
			# payload = {
				# 'assignees': userId, 
				# 'dueDate': -1, 
				# 'content': str(content), 
				# 'description': '', 
				# 'extra': json.dump({
					# "msgId": "6261670174025", 
					# "toUid": "3446346906815329213", 
					# "isGroup": True, 
					# "cliMsgId": "1737769205643", 
					# "msgType": 1, 
					# "mention": [], 
					# "ownerMsgUId": "847453062215452332", 
					# "message": "Đéo má"
				# }), 
				# 'dateDefaultType': 0, 
				# 'status': -1, 
				# 'watchers': '[]', 
				# 'schedule': None, 
				# 'src': 5, 
				# 'imei': 'dfad45f9-dfdc-458c-9aef-d24412093c5c-b78b4e2d6c0a362c418b145fe44ed73f'
			# }
			params = {
				"params": self._encode({
					'assignees': userId, 
					'dueDate': -1, 
					'content': str(content), 
					'description': '', 
					'extra': json.dumps({
						"toUid": str(thread_id),
						"isGroup": True
					}), 
					'dateDefaultType': 0, 
					'status': -1, 
					'src': 5, 
					'imei': self._imei
				}),
				"zpw_ver": 655,
				"zpw_type": 24
			}
			response = self._post("https://board-wpa.chat.zalo.me/api/board/personal/todo/create", params=params)
			data = response.json()
			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("data") else results
				if results == None:
					results = {"error_code": 1337, "error_message": "Data is None"}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						results = {"error_code": 1337, "error_message": results}

				return Group.fromDict(results, None)

			error_code = data.get("error_code")
			error_message = data.get("error_message") or data.get("data")
			raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

		else:
			raise ZaloUserError("Thread type is invalid")

	def todo_nochat(self, userId, content, thread_id, thread_type):
		"""Assign todo user by ID or Group ID

			Args:
				userId (int | str): User ID to todo
				content (str): Content to assign work
				thread_id (int | str): User/Group ID to send to.
				thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			Idk

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(userId, list):
			userId = [str(userid) for userid in userId]
		else:
			userId = [str(userId)]

		if thread_type == ThreadType.USER:
			params = {
				"params": self._encode({
					'assignees': userId, 
					'dueDate': -1, 
					'content': str(content), 
					'description': '', 
					'extra': json.dumps({
						"toUid": thread_id, 
						"msgId": "64" + ''.join(random.choices('0123456789', k=12)), 
						"isGroup": False, 
						"cliMsgId": str(_util.now()), 
						"msgType": 1, 
						"mention": [], 
						"ownerMsgUId": self.uid
					}), 
					'dateDefaultType': 0, 
					'status': -1, 
					'watchers': '[]', 
					'schedule': None, 
					'src': 2, 
					'imei': self._imei
				}),
				"zpw_ver": 655,
				"zpw_type": 24
			}

			response = self._post("https://task-api.chat.zalo.me/api/board/personal/todo/create", params=params)
			data = response.json()
			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("data") else results
				if results == None:
					results = {"error_code": 1337, "error_message": "Data is None"}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						results = {"error_code": 1337, "error_message": results}

				return Group.fromDict(results, None)

			error_code = data.get("error_code")
			error_message = data.get("error_message") or data.get("data")
			raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

		elif thread_type == ThreadType.GROUP:
			params = {
				"params": self._encode({
					'assignees': userId, 
					'dueDate': -1, 
					'content': str(content), 
					'description': '', 
					'extra': json.dumps({
						"msgId": "5970078638525",
						"toUid": str(thread_id),
						"isGroup": True,
						"cliMsgId": _util.now(),
						"msgType": 1,
						"mention": [],
						"ownerMsgUId": 847453062215452332,
						"message": str(content)
					}), 
					'dateDefaultType': 0, 
					'status': -1, 
					'watchers': '[]', 
					'schedule': None, 
					'src': 5, 
					'imei': self._imei
				}),
				"zpw_ver": 655,
				"zpw_type": 24
			}
			response = self._post("https://board-wpa.chat.zalo.me/api/board/personal/todo/create", params=params)
			data = response.json()
			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("data") else results
				if results == None:
					results = {"error_code": 1337, "error_message": "Data is None"}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						results = {"error_code": 1337, "error_message": results}

				return Group.fromDict(results, None)

			error_code = data.get("error_code")
			error_message = data.get("error_message") or data.get("data")
			raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		else:
			raise ZaloUserError("Thread type is invalid")


	def todozlapp(self, userId, content, thread_id, thread_type):
		"""Assign todo user by ID or Group ID

			Args:
				userId (int | str): User ID to todo
				content (str): Content to assign work
				thread_id (int | str): User/Group ID to send to.
				thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			Idk

		Raises:
			ZaloAPIException: If request failed
		"""
		try:
			if isinstance(userId, list):
				userId = [str(userid) for userid in userId]
			else:
				userId = [str(userId)]

			params = {
				"zpw_ver": "655",
				"zpw_type": "24",
				"ref": "https://task.chat.zalo.me"
			}
			url = "https://task-api.chat.zalo.me/api/board/personal/todo/create"

			if thread_type == ThreadType.USER:
				payload = {
					"params": json.dumps({
						'assignees': userId, 
						'dueDate': -1, 
						'content': str(content), 
						'description': '', 
						'extra': json.dumps({
							"toUid": str(thread_id),
							"cliMsgId": str(_util.now()),
							"msgType": 1,
							"msgId": "6023784351406",
							"isGroup": False
						}), 
						'dateDefaultType': 0, 
						'status': -1, 
						'watchers': [], 
						'src': 10
					})
				}
				response = self._post(url, params=params, data=payload)
				data = response.json()


			elif thread_type == ThreadType.GROUP:
				payload = {
					"params": json.dumps({
						'assignees': userId, 
						'dueDate': -1, 
						'content': str(content), 
						'description': '', 
						'extra': json.dumps({
							"toUid": str(thread_id),
							"isGroup": True
						}), 
						'dateDefaultType': 0, 
						'status': -1, 
						'watchers': '[]', 
						'src': 2
					})
				}
				response = self._post(url, params=params, data=payload)
				data = response.json()
			return Group.fromDict(data, None)
		except Exception as e:
			traceback.print_exc()


	"""
	END USER ACTION METHODS
	"""

	"""
	GROUP ACTION METHODS
	"""

	def createGroup(self, name=None, description=None, members=[], nameChanged=1, createLink=1, groupType=1):
		"""Create a new group.

		Args:
			name (str): The new group name
			description (str): Description of the new group
			members (str | list): List/String member IDs add to new group
			nameChanged (int - auto): Will use default name if disabled (0), else (1)
			createLink (int - default): Create a group link? Default = 1 (True)
			groupType (int - default): Create community or normal group? Default = 1 (normal), 2 (community)

		Returns:
			object: `Group` new group response
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		memberTypes = []
		nameChanged = 1 if name else 0
		name = name or "Default Group Name"
		groupType = int(groupType)

		if members and isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]

		if members:
			for i in members:
				memberTypes.append(-1)

		params = {
			"params": self._encode({
				"clientId": _util.now(),
				"gname": name,
				"gdesc": description,
				"members": members,
				"memberTypes": memberTypes,
				"nameChanged": nameChanged,
				"createLink": createLink,
				"clientLang": "vi",
				"imei": self._imei,
				"groupType": groupType,
				"zsource": -1
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}
		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/create/v2", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def timeDay(self):
		today = datetime.date.today()
		midnight = datetime.datetime.combine(today, datetime.time(0, 0))
		timestamp_milliseconds = int(midnight.timestamp() * 1000)
		return timestamp_milliseconds

	def changeGroupAvatar(self, filePath, groupId):
		"""Upload/Change group avatar by ID.

		Client must be the Owner of the group
		(If the group does not allow members to upload/change)

		Args:
			filePath (str): A path to the image to upload/change avatar
			groupId (int | str): Group ID to upload/change avatar

		Returns:
			object: `Group` Group avatar change status
			None: If requet success/failed depending on the case
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if not os.path.exists(filePath):
			raise ZaloUserError(f"{filePath} not found")


		files = [("fileContent", open(filePath, "rb"))]

		params = {
			"params": self._encode({
				"grid": str(groupId),
				"avatarSize": 120,
				"clientId": "g" + str(groupId) + _util.formatTime("%H:%M %d/%m/%Y"),
				"originWidth": 640,
				"originHeight": 640,
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._post("https://tt-files-wpa.chat.zalo.me/api/group/upavatar", params=params, files=files)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def changeGroupName(self, groupName, groupId):
		"""Set/Change group name by ID.

		Client must be the Owner of the group
		(If the group does not allow members to change group name)

		Args:
			groupName (str): Group name to change
			groupId (int | str): Group ID to change name

		Returns:
			object: `Group` Group name change status
			None: If requet success/failed depending on the case
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"gname": groupName,
				"grid": str(groupId)
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/updateinfo", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def changeGroupDesc(self, groupDesc, groupId):
		"""Not Available Yet"""

	def changeGroupSetting(self, groupId, defaultMode="default", **kwargs):
		"""Update group settings by ID.

		Client must be the Owner/Admin of the group.

		Warning:
			Other settings will default value if not set. See `defaultMode`

		Args:
			groupId (int | str): Group ID to update settings
			defaultMode (str): Default mode of settings

				default: Group default settings
				anti-raid: Group default settings for anti-raid

			**kwargs: Group settings kwargs, Value: (1 = True, 0 = False)

				blockName: Không cho phép user đổi tên & ảnh đại diện nhóm
				signAdminMsg: Đánh dấu tin nhắn từ chủ/phó nhóm
				addMemberOnly: Chỉ thêm members (Khi tắt link tham gia nhóm)
				setTopicOnly: Cho phép members ghim (tin nhắn, ghi chú, bình chọn)
				enableMsgHistory: Cho phép new members đọc tin nhắn gần nhất
				lockCreatePost: Không cho phép members tạo ghi chú, nhắc hẹn
				lockCreatePoll: Không cho phép members tạo bình chọn
				joinAppr: Chế độ phê duyệt thành viên
				bannFeature: Default (No description)
				dirtyMedia: Default (No description)
				banDuration: Default (No description)
				lockSendMsg: Không cho phép members gửi tin nhắn
				lockViewMember: Không cho phép members xem thành viên nhóm
				blocked_members: Danh sách members bị chặn

		Returns:
			object: `Group` Group settings change status
			None: If requet success/failed depending on the case
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if defaultMode == "anti-raid":
			defSetting = {
				"blockName": 1,
				"signAdminMsg": 1,
				"addMemberOnly": 1,
				"setTopicOnly": 1,
				"enableMsgHistory": 1,
				"lockCreatePost": 1,
				"lockCreatePoll": 1,
				"joinAppr": 1,
				"bannFeature": 1,
				"dirtyMedia": 1,
				"banDuration": 1,
				"lockSendMsg": 0,
				"lockViewMember": 1,
			}
		elif defaultMode == "silent":
			defSetting = {
				"blockName": 1,
				"signAdminMsg": 1,
				"addMemberOnly": 1,
				"setTopicOnly": 1,
				"enableMsgHistory": 1,
				"lockCreatePost": 1,
				"lockCreatePoll": 1,
				"joinAppr": 1,
				"bannFeature": 1,
				"dirtyMedia": 1,
				"banDuration": 1,
				"lockSendMsg": 1,
				"lockViewMember": 1,
			}
		elif defaultMode == "allow":
			defSetting = {
				"blockName": 0,
				"signAdminMsg": 1,
				"addMemberOnly": 0,
				"setTopicOnly": 0,
				"enableMsgHistory": 1,
				"lockCreatePost": 0,
				"lockCreatePoll": 0,
				"joinAppr": 0,
				"bannFeature": 0,
				"dirtyMedia": 0,
				"banDuration": 0,
				"lockSendMsg": 0,
				"lockViewMember": 0,
			}
		else:
			defSetting = self.fetchGroupInfo(groupId).gridInfoMap
			defSetting = defSetting[str(groupId)]["setting"]

		blockName = kwargs.get("blockName", defSetting.get("blockName", 1))
		signAdminMsg = kwargs.get("signAdminMsg", defSetting.get("signAdminMsg", 1))
		addMemberOnly = kwargs.get("addMemberOnly", defSetting.get("addMemberOnly", 0))
		setTopicOnly = kwargs.get("setTopicOnly", defSetting.get("setTopicOnly", 1))
		enableMsgHistory = kwargs.get("enableMsgHistory", defSetting.get("enableMsgHistory", 1))
		lockCreatePost = kwargs.get("lockCreatePost", defSetting.get("lockCreatePost", 1))
		lockCreatePoll = kwargs.get("lockCreatePoll", defSetting.get("lockCreatePoll", 1))
		joinAppr = kwargs.get("joinAppr", defSetting.get("joinAppr", 1))
		bannFeature = kwargs.get("bannFeature", defSetting.get("bannFeature", 0))
		dirtyMedia = kwargs.get("dirtyMedia", defSetting.get("dirtyMedia", 0))
		banDuration = kwargs.get("banDuration", defSetting.get("banDuration", 0))
		lockSendMsg = kwargs.get("lockSendMsg", defSetting.get("lockSendMsg", 0))
		lockViewMember = kwargs.get("lockViewMember", defSetting.get("lockViewMember", 0))
		blocked_members = kwargs.get("blocked_members", [])

		params = {
			"params": self._encode({
				"blockName": blockName,
				"signAdminMsg": signAdminMsg,
				"addMemberOnly": addMemberOnly,
				"setTopicOnly": setTopicOnly,
				"enableMsgHistory": enableMsgHistory,
				"lockCreatePost": lockCreatePost,
				"lockCreatePoll": lockCreatePoll,
				"joinAppr": joinAppr,
				"bannFeature": bannFeature,
				"dirtyMedia": dirtyMedia,
				"banDuration": banDuration,
				"lockSendMsg": lockSendMsg,
				"lockViewMember": lockViewMember,
				"blocked_members": blocked_members,
				"grid": str(groupId),
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/setting/update", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def changeGroupOwner(self, newAdminId, groupId):
		"""Change group owner (yellow key) by ID.

		Client must be the Owner of the group.

		Args:
			newAdminId (int | str): members ID to changer owner
			groupId (int | str): ID of the group to changer owner

		Returns:
			object: `Group` Group owner change status
			None: If requet success/failed depending on the case
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"params": self._encode({
				"grid": str(groupId),
				"newAdminId": str(newAdminId),
				"imei": self._imei,
				"language": "vi"
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/change-owner", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def addUsersToMultiGroup(self, user_id, group_ids):
		"""Định nghĩa của người có tri thức có thể làm phiền được mọi người"""

		if group_ids and isinstance(group_ids, list):
			groupId = [str(group) for group in group_ids]
		else:
			groupId = [str(group_ids)]

		params = {
			"zpw_ver": 662,
			"zpw_type": 24,
			"params": self._encode({
				'grids': groupId, 
				'member': user_id, 
				'memberType': -1, 
				'srcInteraction': 2, 
				'clientLang': 'vi'
			})
		}
		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/invite/multi", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")



	def addUsersToGroupV2(self, user_ids, groupId, memberTypes=[-1]):
		"""Add friends/users to a group.

		Args:
			user_ids (str | list): One or more friend/user IDs to add
			groupId (int | str): Group ID to add friend/user to

		Returns:
			object: `Group` add friend/user data
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		
		if user_ids and isinstance(user_ids, list):
			members = [str(user) for user in user_ids]
		else:
			members = [str(user_ids)]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"grid": str(groupId),
				"members": members,
				"memberTypes": memberTypes,
				"imei": self._imei,
				"clientLang": "vi"
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/invite/v2", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def addUsersToGroupV1(self, user_ids, groupId, memberTypes=[-1]):
		"""Add friends/users to a group.

		Args:
			user_ids (str | list): One or more friend/user IDs to add
			groupId (int | str): Group ID to add friend/user to

		Returns:
			object: `Group` add friend/user data
			dict: A dictionary containing error_code, response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		
		if user_ids and isinstance(user_ids, list):
			members = [str(user) for user in user_ids]
		else:
			members = [str(user_ids)]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"grid": str(groupId),
				"members": members
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/invite", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def kickUsersInGroup(self, members, groupId):
		"""Kickout members in group by ID.

		Client must be the Owner of the group.

		Args:
			members (str | list): One or More member IDs to kickout
			groupId (int | str): Group ID to kick member from

		Returns:
			object: `Group` kick data
			dict: A dictionary/object containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"grid": str(groupId),
				"members": members
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/kickout", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def blockUsersInGroup(self, members, groupId):
		"""Blocked members in group by ID.

		Client must be the Owner of the group.

		Args:
			members (str | list): One or More member IDs to block
			groupId (int | str): Group ID to block member from

		Returns:
			object: `Group` block members response
			dict: A dictionary/object containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"grid": str(groupId),
				"members": members
			})
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/blockedmems/add", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def unblockUsersInGroup(self, members, groupId):
		"""unblock members in group by ID.

		Client must be the Owner of the group.

		Args:
			members (str | list): One or More member IDs to unblock
			groupId (int | str): Group ID to unblock member from

		Returns:
			object: `Group` unblock members response
			dict: A dictionary/object containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"grid": str(groupId),
				"members": members
			})
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/blockedmems/remove", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def addGroupAdmins(self, members, groupId):
		"""Add admins to the group (white key).

		Client must be the Owner of the group.

		Args:
			members (str | list): One or More member IDs to add
			groupId (int | str): Group ID to add admins

		Returns:
			object: `Group` Group admins add status
			None: If requet success/failed depending on the case
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]

		params = {
			"params": self._encode({
				"grid": str(groupId),
				"members": members,
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/admins/add", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def removeGroupAdmins(self, members, groupId):
		"""Remove admins in the group (white key) by ID.

		Client must be the Owner of the group.

		Args:
			members (str | list): One or More admin IDs to remove
			groupId (int | str): Group ID to remove admins

		Returns:
			object: `Group` Group admins remove status
			None: If requet success/failed depending on the case
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]

		params = {
			"params": self._encode({
				"grid": str(groupId),
				"members": members,
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/admins/remove", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def pinGroupMsg(self, pinMsg, groupId):
		"""Pin message in group by ID.

		Args:
			pinMsg (Message): Message Object to pin
			groupId (int | str): Group ID to pin message

		Returns:
			object: `Group` pin message status
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"grid": str(groupId),
				"type": 2,
				"color": -14540254,
				"emoji": "📌",
				"startTime": -1,
				"duration": -1,
				"repeat": 0,
				"src": -1,
				"imei": self._imei,
				"pinAct": 1
			}
		}

		if pinMsg.msgType == "webchat":

			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"title": pinMsg.content,
				"msg_type": _util.getClientMessageType(pinMsg.msgType)
			})

		elif pinMsg.msgType == "chat.voice":

			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"msg_type": _util.getClientMessageType(pinMsg.msgType)
			})

		elif pinMsg.msgType in ["chat.photo", "chat.video.msg"]:

			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"thumb": pinMsg.content.thumb,
				"title": pinMsg.content.description,
				"msg_type": _util.getClientMessageType(pinMsg.msgType)
			})

		elif pinMsg.msgType == "chat.sticker":

			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"extra": json.dumps({
					"id": pinMsg.content.id,
					"catId": pinMsg.content.catId,
					"type": pinMsg.content.type
				}),
				"msg_type": _util.getClientMessageType(pinMsg.msgType)
			})

		elif pinMsg.msgType in ["chat.recommended", "chat.link"]:

			extra = json.loads(pinMsg.content.params)
			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"href": pinMsg.content.href,
				"thumb": pinMsg.content.thumb or "",
				"title": pinMsg.content.title,
				"linkCaption": "https://vrxx1337.vercel.app",
				"redirect_url": extra.get("redirect_url", ""),
				"streamUrl": extra.get("streamUrl", ""),
				"artist": extra.get("artist", ""),
				"stream_icon": extra.get("stream_icon", ""),
				"type": 2,
				"extra": json.dumps({
					"action": pinMsg.content.action,
					"params": json.dumps({
						"mediaTitle": extra.get("mediaTitle", ""),
						"artist": extra.get("artist", ""),
						"src": extra.get("src", ""),
						"stream_icon": extra.get("stream_icon", ""),
						"streamUrl": extra.get("streamUrl", ""),
						"type": 2
					})
				}),
				"msg_type": _util.getClientMessageType(pinMsg.msgType)
			})

		elif pinMsg.msgType == "chat.location.new":

			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"msg_type": _util.getClientMessageType(pinMsg.msgType),
				"title": pinMsg.content.title or pinMsg.content.description
			})

		elif pinMsg.msgType == "share.file":

			extra = json.loads(pinMsg.content.params)
			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"title": pinMsg.content.title,
				"extra": json.dumps({
					"fileSize": "7295",
					"checksum": extra.get("checksum", ""),
					"fileExt": extra.get("fileExt", ""),
					"tWidth": extra.get("tWidth", 0),
					"tHeight": extra.get("tHeight", 0),
					"duration": extra.get("duration", 0),
					"fType": extra.get("fType", 0),
					"fdata": extra.get("fdata", ""),
				}),
				"msg_type": _util.getClientMessageType(pinMsg.msgType)
			})

		elif pinMsg.msgType == "chat.gif":

			payload["params"]["params"] = json.dumps({
				"client_msg_id": pinMsg.cliMsgId,
				"global_msg_id": pinMsg.msgId,
				"senderUid": str(int(pinMsg.uidFrom) or self.uid),
				"senderName": pinMsg.dName,
				"thumb": pinMsg.content.thumb,
				"msg_type": _util.getClientMessageType(pinMsg.msgType)
			})

		payload["params"] = self._encode(payload["params"])
		response = self._post("https://groupboard-wpa.chat.zalo.me/api/board/topic/createv2", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def unpinGroupMsg(self, pinId, pinTime, groupId):
		"""Unpin message in group by ID.

		Args:
			pinId (int | str): Pin ID to unpin
			pinTime (int): Pin start time
			groupId (int | str): Group ID to unpin message

		Returns:
			object: `Group` unpin message status
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				"grid": str(groupId),
				"imei": self._imei,
				"topic": {
					"topicId": str(pinId),
					"topicType": 2
				},
				"boardVersion": int(pinTime)
			})
		}

		response = self._get("https://groupboard-wpa.chat.zalo.me/api/board/unpinv2", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def deleteMsg(self, msgId, ownerId, clientMsgId, thread_id, thread_type, onlyMe=0, destId=None):
		"""Delete message in group by ID.

		Args:
			groupId (int | str): Group ID to delete message
			msgId (int | str): Message ID to delete
			ownerId (int | str): Owner ID of the message to delete
			clientMsgId (int | str): Client message ID to delete message

		Returns:
			object: `Group` delete message status
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		if destId:
			destId = destId
		else:
			destId = thread_id
		if thread_type == ThreadType.USER:
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"nretry": 0

			}
			payload = {
				"params": self._encode({
					'toid': str(thread_id), 
					'cliMsgId': str(_util.now()), 
					'msgs': [{
						'cliMsgId': str(clientMsgId), 
						'globalMsgId': str(msgId), 
						'ownerId': str(ownerId), 
						'destId': str(destId)
					}], 
					'onlyMe': onlyMe, 
					'imei': self._imei
				})
			}
			url = "https://tt-chat1-wpa.chat.zalo.me/api/message/delete"
		else:
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"nretry": 0
			}

			payload = {
				"params": self._encode({
					"grid": str(thread_id),
					"cliMsgId": str(_util.now()),
					"msgs": [{
						"cliMsgId": str(clientMsgId),
						"globalMsgId": str(msgId),
						"ownerId": str(ownerId),
						"destId": str(thread_id)
					}],
					"onlyMe": onlyMe
				})
			}
			url = "https://tt-group-wpa.chat.zalo.me/api/group/deletemsg"
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
	
	def deleteMultiMsg(self, msgData, thread_id, thread_type, onlyMe=0, destId=None):
		"""Delete message in group by ID.

		Args:
			groupId (int | str): Group ID to delete message
			msgId (int | str): Message ID to delete
			ownerId (int | str): Owner ID of the message to delete
			clientMsgId (int | str): Client message ID to delete message

		Returns:
			object: `Group` delete message status
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		if destId:
			destId = destId
		else:
			destId = thread_id
			
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": self._encode({
				"grid": str(thread_id),
				"cliMsgId": str(_util.now()),
				"msgs": msgData,
				"onlyMe": onlyMe
			})
		}
		url = "https://tt-group-wpa.chat.zalo.me/api/group/deletemsg"
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def deleteGroupConver(self, msgId, ownerId, clientMsgId, groupId, onlyMe=0):
		"""Delete message in group by ID.

		Args:
			groupId (int | str): Group ID to delete message
			msgId (int | str): Message ID to delete
			ownerId (int | str): Owner ID of the message to delete
			clientMsgId (int | str): Client message ID to delete message

		Returns:
			object: `Group` delete message status
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": self._encode({
				'grid': str(groupId), 
				'cliMsgId': str(_util.now()), 
				'conver': {
					'ownerId': str(ownerId), 
					'cliMsgId': str(clientMsgId), 
					'globalMsgId': str(msgId)
				}, 
				'onlyMe': onlyMe, 
				'imei': self._imei
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/deleteconver", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")


	def deleteGroupMsgBypassAntiUndo(self, msgId, ownerId, clientMsgId, groupId, onlyMe=0):
		"""Delete message in group by ID. SUPPORT BYPASS MẤY THẰNG TRẨU TRE RẢI LINK CÓ ANTI UNDO ( NPH ) là ví dụ

		Args:
			groupId (int | str): Group ID to delete message
			msgId (int | str): Message ID to delete
			ownerId (int | str): Owner ID of the message to delete
			clientMsgId (int | str): Client message ID to delete message

		Returns:
			object: `Group` delete message status
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"grid": str(groupId),
				"cliMsgId": str(_util.now()),
				"msgs": [{
					"cliMsgId": str(_util.now()),
					"globalMsgId": str(msgId),
					"ownerId": str(ownerId),
					"destId": str(groupId)
				}],
				"onlyMe": onlyMe
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/deletemsg", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def deleteGroupMsgBypassAntiUndoSpecial(self, msgId, ownerId, clientMsgId, groupId, onlyMe=0):
		"""Delete message in group by ID. SUPPORT BYPASS MẤY THẰNG TRẨU TRE RẢI LINK CÓ ANTI UNDO ( NPH ) là ví dụ

		Args:
			groupId (int | str): Group ID to delete message
			msgId (int | str): Message ID to delete
			ownerId (int | str): Owner ID of the message to delete
			clientMsgId (int | str): Client message ID to delete message

		Returns:
			object: `Group` delete message status
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"grid": str(groupId),
				"cliMsgId": str(_util.now() + int(clientMsgId)),
				"msgs": [{
					"cliMsgId": str(clientMsgId),
					"globalMsgId": str(msgId),
					"ownerId": str(ownerId),
					"destId": str(groupId)
				}],
				"onlyMe": onlyMe
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/deletemsg", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def viewGroupPending(self, groupId):
		"""See list of people pending approval in group by ID.

		Args:
			groupId (int | str): Group ID to view pending members

		Returns:
			object: `Group` pending responses
			dict: A dictionary containing error_code & responses if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"params": self._encode({
				"grid": str(groupId),
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/pending-mems/list", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def handleGroupPending(self, members, groupId, isApprove=True):
		if isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]

		for i in range(0, len(members), 200):
			chunk = members[i:i + 200]
			self._handleGroupPending(members=members, groupId=groupId, isApprove=isApprove)
			
	def _handleGroupPending(self, members, groupId, isApprove=True):
		"""Approve/Deny pending users to the group from the group's approval.

		Client must be the Owner of the group.

		Args:
			members (str | list): One or More member IDs to handle
			groupId (int | str): ID of the group to handle pending members
			isApprove (bool): Approve/Reject pending members (True | False)

		Returns:
			object: `Group` handle pending responses
			dict: A dictionary/object containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(members, list):
			members = [str(member) for member in members]
		else:
			members = [str(members)]
		
		
		params = {
			"params": self._encode({
				"grid": str(groupId),
				"members": members,
				"isApprove": 1 if isApprove else 0
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/pending-mems/review", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def viewPollDetail(self, pollId):
		"""View poll data by ID.

		Args:
			pollId (int | str): Poll ID to view detail

		Returns:
			object: `Group` poll data
			dict: A dictionary containing error_code & response if failed

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"params": self._encode({
				"poll_id": int(pollId),
				"imei": self._imei
			}),
			"zpw_ver": 655,
			"zpw_type": 24
		}

		response = self._get("https://tt-group-wpa.chat.zalo.me/api/poll/detail", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def createPoll(
		self,
		question,
		options,
		groupId,
		expiredTime=0,
		pinAct=False,
		multiChoices=True,
		allowAddNewOption=True,
		hideVotePreview=False,
		isAnonymous=False
	):
		"""Create poll in group by ID.

		Client must be the Owner of the group.

		Args:
			question (str): Question for poll
			options (str | list): List options for poll
			groupId (int | str): Group ID to create poll from
			expiredTime (int): Poll expiration time (0 = no expiration)
			pinAct (bool): Pin action (pin poll)
			multiChoices (bool): Allows multiple poll choices
			allowAddNewOption (bool): Allow members to add new options
			hideVotePreview (bool): Hide voting results when haven't voted
			isAnonymous (bool): Hide poll voters

		Returns:
			object: `Group` poll create data
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"group_id": str(groupId),
				"question": question,
				"options": [],
				"expired_time": expiredTime,
				"pinAct": pinAct,
				"allow_multi_choices": multiChoices,
				"allow_add_new_option": allowAddNewOption,
				"is_hide_vote_preview": hideVotePreview,
				"is_anonymous": isAnonymous,
				"poll_type": 0,
				"src": 1,
				"imei": self._imei
			}
		}

		if isinstance(options, list):
			payload["params"]["options"] = options
		else:
			payload["params"]["options"].append(str(options))

		payload["params"] = self._encode(payload["params"])

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/poll/create", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def lockPoll(self, pollId):
		"""Lock/end poll in group by ID.

		Client must be the Owner of the group.

		Args:
			pollId (int | str): Poll ID to lock

		Returns:
			None: If requet success
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"poll_id": int(pollId),
				"imei": self._imei
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/poll/end", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def upgradeComunity(self, groupId):
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'grId': str(groupId), 
				'language': 'vi'
			})
		}
		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/upgrade/community", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			# print(results)
			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def getQuotaCommunity(self, groupId):
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'grId': str(groupId), 
				'language': 'vi'
			})
		}
		response = self._get("https://tt-group-wpa.chat.zalo.me/api/group/quota/community", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			# print(results)
			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
	
	def disperseGroup(self, groupId):
		"""Disperse group by ID.

		Client must be the Owner of the group.

		Args:
			groupId (int | str): Group ID to disperse

		Returns:
			None: If requet success
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": self._encode({
				"grid": str(groupId),
				"imei": self._imei
			})
		}

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/disperse", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	"""
	END GROUP ACTION METHODS
	"""

	"""
	SEND METHODS
	"""

	def send(self, message, thread_id, thread_type=ThreadType.USER, mark_message=None, ttl=0):
		"""Send message to a thread.

		Args:
			message (Message): Message to send
			thread_id (int | str): User/Group ID to send to
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` (Returns msg ID just sent)
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		thread_id = str(int(thread_id) or self.uid)
		if message.mention:
			# print(message.mention)
			return self.sendMentionMessage(message, thread_id, ttl)
		else:
			return self.sendMessage(message, thread_id, thread_type, mark_message, ttl)

	def sendMessage(self, message, thread_id, thread_type, mark_message=None, ttl=0, zpw_type=30):
		"""Send message to a thread (user/group).

		Args:
			message (Message): Message to send
			thread_id (int | str): User/Group ID to send to
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP
			mark_message (str): Send messages as `Urgent` or `Important` mark

		Returns:
			object: `User/Group` (Returns msg ID just sent)
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		
		params = {
			"zpw_ver": 671,
			"zpw_type": zpw_type,
			"nretry": 0
		}
		
		payload = {
			"params": {
				"message": message.text,
				"clientId": _util.now() + 3000,
				"imei": self._imei,
				"ttl": ttl
			}
		}
		print(payload)
		if mark_message and mark_message.lower() in ["important", "urgent"]:
			markType = 1 if mark_message.lower() == "important" else 2
			payload["params"]["metaData"] = {"urgency": markType}
		
		if message.style:
			payload["params"]["textProperties"] = message.style
			
		if thread_type == ThreadType.USER:
			url = "https://tt-chat2-wpa.chat.zalo.me/api/message/sms"
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/sendmsg"
			payload["params"]["visibility"] = 10
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")
		
		payload["params"] = self._encode(payload["params"])
		
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def forwardMessage(self, message, thread_ids, thread_type, ttl=0):
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 670,
			"zpw_type": 24
		}
		if isinstance(thread_ids, list):
			toIds = [{"clientId": _util.now() + i, "toUid": str(thread_id), "ttl": ttl} for i, thread_id in enumerate(thread_ids)]
		else:
			toIds = [{"clientId": _util.now() + 3000, "toUid": str(thread_ids), "ttl": ttl}]
			
		# print(toIds)
		payload = {
			"params": self._encode({
				"toIds": toIds, 
				"imei": self._imei, 
				"ttl": ttl, 
				"msgType": "1", 
				"totalIds": len(toIds), 
				"msgInfo": json.dumps({
					"message": message.text
				})
			})
		}
		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/mforward"
		else:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/mforward"
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def acpCall(self, payload, thread_type):
		"""Accept incoming calls with full payload information.

		Returns:
			object: `Data` Request information sent

		Raises:
			ZaloAPIException: If request failed
		"""
		
		if thread_type == ThreadType.USER:
			params = {
				"zpw_ver": 655,
				"zpw_type": 24
			}
			payload = {
				"params": payload
			}
			payload["params"] = self._encode(payload["params"])
			
			response = self._post("https://voicecall-wpa.chat.zalo.me/api/voicecall/answer", params=params, data=payload)
		else:
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": self._encode(payload)
			}
			response = self._get("https://voicecall-wpa.chat.zalo.me/api/voicecall/group/answer", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
	
	def dumps_template(self, data: dict) -> str:
		noise_list = data.get("noiseId", [])
		noise_str = "[ " + ", ".join(f'\\"{x}\\"' for x in noise_list) + " ]\\\\n"
		return f'\n{{\n\t"extraData" : "{data.get("extraData", "")}",\n\t"groupAvatar" : "{data.get("groupAvatar", "")}",\n\t"groupId" : "{{groupId}}",\n\t"groupName" : "{data.get("groupName", "")}",\n\t"maxUsers" : {data.get("maxUsers", 0)},\n\t"noiseId" : "{noise_str}"\n}}\n'
		
		
	def dict_to_custom_string(self, d):
		keys = ["callType", "duration", "extraData", "groupId"]
		lines = []
		for k in keys:
			v = d.get(k, "")
			if isinstance(v, str):
				val = f'"{v}"'
			elif v is None:
				val = 'null'
			else:
				val = str(v)
			lines.append(f'\t"{k}" : {val},')
		lines[-1] = lines[-1].rstrip(',')
		return '\n{\n' + '\n'.join(lines) + '\n}\n'
		
	def cancelCall(self, groupId, callId, hostCall):
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'callId': callId, 
				'hostCall': hostCall, 
				'data': self.dict_to_custom_string({
					"callType": 1, 
					"duration": 0, 
					"extraData": "", 
					"groupId": groupId
				})
			})
		}
		response = self._get("https://voicecall-wpa.chat.zalo.me/api/voicecall/group/cancel", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def reqCall(self, groupId, userCall, groupName="Tahn Dev Is Very Handsome!!!"):
		"""Accept incoming calls with full payload information.

		Returns:
			object: `Data` Request information sent

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(userCall, list):
			userCall = [str(member) for member in userCall]
		else:
			userCall = [str(userCall)]
		
		# print(userCall)
		params = {
			"zpw_ver": 667,
			"zpw_type": 24,
			"params": self._encode({
				'groupId': groupId, 
				'callId': int(int(str(int(time.time() * 1000))[3:]) / 8), 
				'typeRequest': 1, 
				'data': self.dumps_template({
					"extraData": "", 
					"groupAvatar": "https://f37-zfcloud.zdn.vn/280cf41f939b32c56b8a/6873672850320027460", 
					"groupId": groupId, 
					"groupName": groupName, 
					"maxUsers": 8, 
					"noiseId": userCall
				}), 
				'partners': str(userCall).replace("'", '"') + "\n"
			})
		}
		response = self._get("https://voicecall-wpa.chat.zalo.me/api/voicecall/group/requestcall", params=params)
		# print(response)
		# print(response.text)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			# print(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def spaces_to_tabs_line(self, line, tab_width=4):
		m = re.match(r'^( *)', line)
		sp = len(m.group(1))
		tabs = sp // tab_width
		return '\t' * tabs + line[sp:]

	def format_to_string(self, input_dict):
		data = input_dict['data']
		lines = []
		lines.append('{')
		escaped_avatar = json.dumps(data['groupAvatar'], ensure_ascii=True)[1:-1]
		lines.append('\t"groupAvatar" : "' + escaped_avatar + '",')
		escaped_groupName = json.dumps(data['groupName'], ensure_ascii=True)[1:-1]
		lines.append('\t"groupName" : "' + escaped_groupName + '",')
		lines.append('\t"hostCall" : ' + str(data['hostCall']) + ',')
		lines.append('\t"maxUsers" : ' + str(data['maxUsers']) + ',')
		noise_ids = data['noiseId']
		noiseId_str = '[ ' + ', '.join(json.dumps(str(id), ensure_ascii=True) for id in noise_ids) + ' ]'
		lines.append('\t"noiseId" : ' + noiseId_str)
		lines.append('}')
		inner_json = '\n'.join(lines)
		data_with_nl = '\n' + inner_json + '\n'
		escaped_data = json.dumps(data_with_nl, ensure_ascii=True)[1:-1]
		formatted_lines = []
		formatted_lines.append('\n{')
		escaped_codec = json.dumps(input_dict['codec'], ensure_ascii=True)[1:-1]
		formatted_lines.append('\t"codec" : "' + escaped_codec + '",')
		formatted_lines.append('\t"data" : "' + escaped_data + '",')
		escaped_extendData = json.dumps(input_dict['extendData'], ensure_ascii=True)[1:-1]
		formatted_lines.append('\t"extendData" : "' + escaped_extendData + '",')
		escaped_rtcpAddress = json.dumps(input_dict['rtcpAddress'], ensure_ascii=True)[1:-1]
		formatted_lines.append('\t"rtcpAddress" : "' + escaped_rtcpAddress + '",')
		escaped_rtcpAddressIPv6 = json.dumps(input_dict['rtcpAddressIPv6'], ensure_ascii=True)[1:-1]
		formatted_lines.append('\t"rtcpAddressIPv6" : "' + escaped_rtcpAddressIPv6 + '",')
		escaped_rtpAddress = json.dumps(input_dict['rtpAddress'], ensure_ascii=True)[1:-1]
		formatted_lines.append('\t"rtpAddress" : "' + escaped_rtpAddress + '",')
		escaped_rtpAddressIPv6 = json.dumps(input_dict['rtpAddressIPv6'], ensure_ascii=True)[1:-1]
		formatted_lines.append('\t"rtpAddressIPv6" : "' + escaped_rtpAddressIPv6 + '"')
		formatted_lines.append('}\n')
		formatted_str = '\n'.join(formatted_lines)
		return formatted_str
	
	def fetchRealUserId(self, userIds):
		# if isinstance(userIds, list):
			# userId = [str(user) for user in userIds]
		# else:
			# userId = [str(userIds)]
		noiseIds = {}
		for i in range(0, len(userIds), 70):
			chunk = userIds[i:i+70]
			data = self.reqCall(groupId=1, userCall=chunk)
			params = data.params
			noiseId = json.loads(params)["noise"]
			noiseIds = {**noiseId, **noiseIds}
		return noiseIds if isinstance(userIds, list) else noiseIds[userIds]
		
	def call(self, groupId, userCall, groupName="Tahn Dev Is Very Handsome!!!"):
		"""Accept incoming calls with full payload information.

		Returns:
			object: `Data` Request information sent

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(userCall, list):
			userCall = [str(member) for member in userCall]
		else:
			userCall = [str(userCall)]
		
		data = self.reqCall(groupId, userCall)
		# print(data)
		paramData = json.loads(data.params)
		hostCall = 30062011#paramData.get("toId")[0]#30062011#paramData.get("toId")[0]#data.hostCall
		# print(hostCall)
		callId = paramData.get("callId")
		rtpAddress = paramData["callSetting"]["servers"][0]["rtpaddr"]
		rtcpAddress = paramData["callSetting"]["servers"][0]["rtcpaddr"]
		rtpAddressIPv6 = paramData["callSetting"]["servers"][0]["rtpaddrIPv6"]
		rtcpAddressIPv6 = paramData["callSetting"]["servers"][0]["rtcpaddrIPv6"]
		session = paramData["callSetting"]["session"]
		# rtpAddress = "127.0.0.1:1234"
		# rtcpAddress = "127.0.0.1:1234"
		# rtpAddressIPv6 = "127.0.0.1:1234"
		# rtcpAddressIPv6 = "127.0.0.1:1234"
		
		params = {
			"zpw_ver": 667,
			"zpw_type": 24,
			"params": self._encode({
				'callId': callId, 
				'callType': 1, 
				'data': self.format_to_string({
					"codec": "", 
					"data": {
						"groupAvatar": "https://f37-zfcloud.zdn.vn/280cf41f939b32c56b8a/6873672850320027460", 
						"groupName": groupName, 
						"hostCall": hostCall, 
						"maxUsers": 8, 
						"noiseId": userCall
					},
					"extendData": "", 
					"rtcpAddress": rtcpAddress, 
					"rtcpAddressIPv6": rtcpAddressIPv6, 
					"rtpAddress": rtpAddress, 
					"rtpAddressIPv6": rtpAddressIPv6
				}), 
				'session': session, 
				'partners': str(userCall).replace("'", '"') + "\n", 
				'groupId': groupId
			})
		}
		response = self._get("https://voicecall-wpa.chat.zalo.me/api/voicecall/group/request", params=params)
		data = response.json()
		# print(data)
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			# print(results)
			results = results.get("data") if results.get("error_code") == 0 else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}
			print(Group.fromDict(results, None))
			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		
	def replyMessage(self, message, replyMsg, thread_id, thread_type, ttl=0, cliMsg=None):
		"""Reply message in group by ID.

		Args:
			message (Message): Message Object to send
			replyMsg (Message): Message Object to reply
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` (Returns msg ID just sent)
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		# User(uidFrom, msgId, cliMsgId, msgType, content, ts)
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}
		if cliMsg != None:
			clientId = cliMsg
		else:
			clientId = _util.now()
		payload = {
			"params": {
				"message": message.text,
				"clientId": clientId,
				"qmsgOwner": str(int(replyMsg.uidFrom) or self.uid),
				"qmsgId": replyMsg.msgId,
				"qmsgCliId": replyMsg.cliMsgId,
				"qmsgType": _util.getClientMessageType(replyMsg.msgType),
				"qmsg": replyMsg.content,
				"qmsgTs": replyMsg.ts,
				"qmsgAttach": json.dumps({}),
				"qmsgTTL": 0,
				"ttl": ttl,
			}
		}
		if replyMsg.mentions:
			if not isinstance(replyMsg.content, str):
				payload["params"]["qmsg"] = replyMsg.content['title']
			payload["params"]["qmsgAttach"] = json.dumps({"mentions": replyMsg.mentions, "properties": replyMsg.propertyExt})
		if not isinstance(replyMsg.content, str):
			if replyMsg.attachHehehehe:
				payload["params"]["qmsgAttach"] = replyMsg.attachHehehehe
				payload["params"]["qmsg"] = replyMsg.content
			else:
				payload["params"]["qmsg"] = replyMsg.content['title']
				payload["params"]["qmsgAttach"] = json.dumps(replyMsg.content.toDict())
		# {
			# 'grid': '4056589710188453000', 
			# 'message': 'Hello World', 
			# 'clientId': 1739676028725, 
			# 'qmsgOwner': '136747183865577510', 
			# 'qmsgId': '6322106108342', 
			# 'qmsgCliId': '1739675949674', 
			# 'qmsgType': 1, 
			# 'qmsg': '@Tahn Dz nxnxxnxnxnxnxnxn', 
			# 'qmsgTs': '1739675949751', 
			# 'visibility': 0, 
			# 'qmsgAttach': json.dumps({
				# "mentions": [{
					# "uid": "6999359933230722273",
					# "pos": 0, 
					# "len": 8, 
					# "type": 0
				# }], 
				# "properties": {
					# "color": -1,
					# "size": -1,
					# "type": -1, 
					# "subType": 0, 
					# "ext": json.dumps({
						# "sSrcType": -1, 
						# "sSrcStr": "", 
						# "msg_warning_type": 0, 
						# "emoji": {
							# "content": 0, 
							# "num": 0, 
							# "uniq": 0, 
							# "first": "", 
							# "last": "", 
							# "most": "", 
							# "text": 1
						# }
					# })
				# }, 
				# "msgBubbleLayoutType": -1
			# }), 
			# 'qmsgTTL': 0, 
			# 'ttl': 0
		# }
		if message.style:
			payload["params"]["textProperties"] = message.style

		if message.mention:
			payload["params"]["mentionInfo"] = message.mention

		if thread_type == ThreadType.USER:
			url = "https://tt-chat2-wpa.chat.zalo.me/api/message/quote"
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/quote"
			payload["params"]["visibility"] = 0
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			results['cliMsgId'] = clientId
			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendMentionMessage(self, message, groupId, ttl=0):
		"""Send message to a group with mention by ID.

		Args:
			mention (str): Mention format data to send
			message (Message): Message to send
			groupId: Group ID to send to.

		Returns:
			object: `User/Group` (Returns msg ID just sent)
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"grid": str(groupId),
				"message": message.text,
				"mentionInfo": message.mention,
				"clientId": _util.now() * 30000,
				"visibility": 0,
				"ttl": ttl
			}
		}

		if message.style:
			payload["params"]["textProperties"] = message.style

		payload["params"] = self._encode(payload["params"])

		response = self._post("https://tt-group-wpa.chat.zalo.me/api/group/mention", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def todoStatus(self, taskId, status=1):
		"""Update todo status with task id

		Args:
			taskId (int | str): task id with todo
			status (int | str): 1 = done, 2 mở lại todo 
		Returns:
			url

		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": self._encode({
				'id': str(taskId), 
				'status': int(status), 
				'imei': self._imei
			})
		}

		response = self._get("https://board-wpa.chat.zalo.me/api/board/personal/todo/status", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def UploadFile(self, filePath, thread_id, thread_type):
		"""Upload data to zalo cloud.

		Args:
			filePath (str): A path to the image to upload/change avatar
			toid (int | str | None): idk

		Returns:
			url
		"""
		if thread_type == ThreadType.USER:
			type = 2
			url = "https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/upload"
		else:
			type = 11
			url = "https://tt-files-wpa.chat.zalo.me/api/group/asyncfile/upload"
		fileSize = os.path.getsize(filePath)
		max_chunk_size = 3145000
		totalSize = fileSize
		
		clientId = _util.now()
		if fileSize > max_chunk_size:
			with open(filePath, 'rb') as f:
				chunk_id = 1
				while chunk := f.read(max_chunk_size):
					if not chunk:
						break

					chunk_file = BytesIO(chunk)
					fileName = os.path.basename(filePath)
					chunk_file.name = fileName
					files = [("chunkContent", chunk_file)]
					params = {
						"zpw_ver": 655,
						"zpw_type": 24,
						"params": {
							'totalChunk': (fileSize // max_chunk_size) + 1,
							'fileName': fileName,
							'clientId': clientId,
							'totalSize': totalSize,
							'imei': self._imei,
							'chunkId': chunk_id
						},
						"type": type
					}
					if thread_type == ThreadType.USER:
						params["params"]["toid"] = thread_id
					else:
						params["params"]["grid"] = thread_id
					params["params"] = self._encode(params["params"])
					response = self._post(url, params=params, files=files)
					json_data = json.loads(response.text)
					data_value = json_data.get("data")
					cc = self._decode(data_value)
					# print(cc)
					chunk_id += 1

				data = cc.get('data')
				client_file_id = data.get('clientFileId')
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': (fileSize // max_chunk_size) + 1,
						'fileName': fileName,
						'clientId': client_file_id,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': (fileSize // max_chunk_size) + 1
					},
					"type": type
				}
				if thread_type == ThreadType.USER:
					params["params"]["toid"] = thread_id
				else:
					params["params"]["grid"] = thread_id

				params["params"] = self._encode(params["params"])
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(data_value)
				data = cc.get('data')
				return data.get('url')
		else:
			files = [("chunkContent", open(filePath, "rb"))]
			fileName = os.path.basename(filePath)
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': 1,
					'fileName': fileName,
					'clientId': _util.now(),
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': 1
				},
				"type": type
			}
			if thread_type == ThreadType.USER:
				params["params"]["toid"] = thread_id
			else:
				params["params"]["grid"] = thread_id

			params["params"] = self._encode(params["params"])
			response = self._post(url, params=params, files=files)
			json_data = json.loads(response.text)
			data_value = json_data.get("data")
			cc = self._decode(data_value)
			data = cc.get('data')
			if data.get('url'):
				return data.get('url')
			else:
				client_file_id = data.get('clientFileId')
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': 1,
						'fileName': fileName,
						'clientId': client_file_id,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': 1
					},
					"type": type
				}
				if thread_type == ThreadType.USER:
					params["params"]["toid"] = thread_id
				else:
					params["params"]["grid"] = thread_id

				params["params"] = self._encode(params["params"])
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(params=data_value)
				# print(cc)
				data = cc.get('data')
				return data.get('url')


	def UploadFileFromBytes(self, fileData, fileName, thread_id, thread_type):
		"""Upload data to zalo cloud from bytes.

			Args:
				fileData (bytes): The bytes data of the file to upload
				fileName (str): The name of the file
				thread_id (int | str | None): The ID of the thread
				thread_type (ThreadType): The type of the thread

			Returns:
				str: The URL of the uploaded file
		"""
		if thread_type == ThreadType.USER:
			type = 2
			url = "https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/upload"
		else:
			type = 11
			url = "https://tt-files-wpa.chat.zalo.me/api/group/asyncfile/upload"

		fileSize = len(fileData)
		max_chunk_size = 3145000
		totalSize = fileSize
			
		clientId = _util.now()

		if fileSize > max_chunk_size:
			chunk_id = 1
			for i in range(0, fileSize, max_chunk_size):
				chunk = fileData[i:i + max_chunk_size]
				chunk_file = BytesIO(chunk)
				chunk_file.name = fileName
				files = [("chunkContent", chunk_file)]
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': (fileSize // max_chunk_size) + 1,
						'fileName': fileName,
						'clientId': clientId,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': chunk_id
					},
					"type": type
				}
				if thread_type == ThreadType.USER:
					params["params"]["toid"] = thread_id
				else:
					params["params"]["grid"] = thread_id
				params["params"] = self._encode(params["params"])
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(data_value)
				# print(cc)
				chunk_id += 1

			data = cc.get('data')
			client_file_id = data.get('clientFileId')
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': (fileSize // max_chunk_size) + 1,
					'fileName': fileName,
					'clientId': client_file_id,
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': (fileSize // max_chunk_size) + 1
				},
				"type": type
			}
			if thread_type == ThreadType.USER:
				params["params"]["toid"] = thread_id
			else:
				params["params"]["grid"] = thread_id
			params["params"] = self._encode(params["params"])
			response = self._post(url, params=params, files=files)
			json_data = json.loads(response.text)
			data_value = json_data.get("data")
			cc = self._decode(data_value)
			# print(cc)
			data = cc.get('data')
			return data.get('url')
		else:
			chunk_file = BytesIO(fileData)
			chunk_file.name = fileName
			files = [("chunkContent", chunk_file)]
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': 1,
					'fileName': fileName,
					'clientId': _util.now(),
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': 1
				},
				"type": type
			}
			if thread_type == ThreadType.USER:
				params["params"]["toid"] = thread_id
			else:
				params["params"]["grid"] = thread_id
			params["params"] = self._encode(params["params"])
			response = self._post(url, params=params, files=files)
			json_data = json.loads(response.text)
			data_value = json_data.get("data")
			cc = self._decode(data_value)
			# print(cc)
			data = cc.get('data')
			if data.get('url'):
				return data.get('url')
			else:
				client_file_id = data.get('clientFileId')
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': 1,
						'fileName': fileName,
						'clientId': client_file_id,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': 1
					},
					"type": type
				}
				if thread_type == ThreadType.USER:
					params["params"]["toid"] = thread_id
				else:
					params["params"]["grid"] = thread_id
				params["params"] = self._encode(params["params"])
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(data_value)
				# print(cc)
				data = cc.get('data')
				return data.get('url')





	async def _fetch_chunk(self, session, url, start, end):
		if headers is None:
			header = {'Range': f'bytes={start}-{end}'}
		else:
			header = {
				"Connection": "keep-alive",
				"sec-ch-ua": "\"Not)A;Brand\";v\u003d\"8\", \"Chromium\";v\u003d\"138\", \"Google Chrome\";v\u003d\"138\"",
				"sec-ch-ua-mobile": "?1",
				"sec-ch-ua-platform": "\"Android\"",
				"Upgrade-Insecure-Requests": "1",
				"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36",
				"Accept": "text/html,application/xhtml+xml,application/xml;q\u003d0.9,image/avif,image/webp,image/apng,*/*;q\u003d0.8,application/signed-exchange;v\u003db3;q\u003d0.7",
				"Sec-Fetch-Site": "same-origin",
				"Sec-Fetch-Mode": "navigate",
				"Sec-Fetch-User": "?1",
				"Sec-Fetch-Dest": "document",
				"Accept-Encoding": "gzip, deflate, br, zstd",
				"Accept-Language": "en-US,en;q\u003d0.9,vi;q\u003d0.8,pt;q\u003d0.7"
			}
			header["Range"] = f"bytes={start}-{end}"
		try:
			async with session.get(url, headers=header, timeout=5) as resp:
				data = await resp.read()
				# print(data.encode())
				return data
		except asyncio.TimeoutError:
			print("timeout error")
			return None
		except aiohttp.ClientError:
			print("aiohttp error")
			return None

	async def _upload_chunk(self, sem, session, upload_url, params, chunk_data, file_name, chunk_id):
		bio = BytesIO(chunk_data)
		# print(chunk_data)
		bio.name = file_name
		data = aiohttp.FormData()
		data.add_field('chunkContent', bio, filename=file_name)
		async with sem:
			try:
				async with session.post(upload_url, params=params, data=data, headers=HEADERS, cookies=self._state.get_cookies(), timeout=5) as resp:
					text = await resp.text()
					# print(text)
					full_resp = json.loads(text)
					lol = self._decode(full_resp['data'])
					print(lol, chunk_id)
					if lol.get("error_code") == 112:
						return chunk_id
					return full_resp
			except asyncio.TimeoutError:
				return chunk_id
			except aiohttp.ClientError:
				return chunk_id

	async def uploadFileAsync(self, file_url, thread_id, thread_type, fileSize=None):
		try:
			sem = asyncio.Semaphore(100)
			async with aiohttp.ClientSession() as session:
				headers = {
					"Connection": "keep-alive",
					"sec-ch-ua": "\"Not)A;Brand\";v\u003d\"8\", \"Chromium\";v\u003d\"138\", \"Google Chrome\";v\u003d\"138\"",
					"sec-ch-ua-mobile": "?1",
					"sec-ch-ua-platform": "\"Android\"",
					"Upgrade-Insecure-Requests": "1",
					"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36",
					"Accept": "text/html,application/xhtml+xml,application/xml;q\u003d0.9,image/avif,image/webp,image/apng,*/*;q\u003d0.8,application/signed-exchange;v\u003db3;q\u003d0.7",
					"Sec-Fetch-Site": "same-origin",
					"Sec-Fetch-Mode": "navigate",
					"Sec-Fetch-User": "?1",
					"Sec-Fetch-Dest": "document",
					"Accept-Encoding": "gzip, deflate, br, zstd",
					"Accept-Language": "en-US,en;q\u003d0.9,vi;q\u003d0.8,pt;q\u003d0.7"
				}
				if fileSize == None:
					try:
						fileSize = int(requests.get(file_url, stream=True, headers=headers, timeout=5).headers.get("Content-Length"))
					except Exception:
						try:
							fileSize = int(requests.get(file_url, stream=True, timeout=5).headers.get("Content-Length"))
							if fileSize == 0:
								1/0
						except Exception:
							try:
								fileSize = int(requests.head(file_url, stream=True, headers=headers, timeout=5).headers.get("Content-Length"))
								if fileSize == 0:
									1/0
							except Exception:
								try:
									fileSize = int(requests.head(file_url, stream=True, timeout=5).headers.get("Content-Length"))
									if fileSize == 0:
										1/0
								except Exception:
									fileSize = 0
									
				print(fileSize)
				file_name = os.path.basename(urlparse(file_url).path) or 'upload.bin'
				chunk_size = 3145000
				if fileSize > 100000000:
					totalSize = chunk_size
				elif fileSize < 100000000:
					totalSize = fileSize
				print(totalSize)
				total_chunks = math.ceil(fileSize / chunk_size)
				client_id = _util.now()
				if thread_type == ThreadType.USER:
					upload_url = 'https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/upload'
					id_field = 'toid'
				else:
					upload_url = 'https://tt-files-wpa.chat.zalo.me/api/group/asyncfile/upload'
					id_field = 'grid'
				tasks = []
				for idx in range(total_chunks):
					start = idx * chunk_size
					end = min(start + chunk_size - 1, fileSize - 1)
					chunk_id = idx + 1
					params = {
						'zpw_ver': 655,
						'zpw_type': 24,
						'params': {
							'totalChunk': total_chunks,
							'fileName': file_name,
							'clientId': client_id,
							'totalSize': totalSize,
							'imei': self._imei,
							'chunkId': chunk_id,
							id_field: thread_id
						},
						'type': 2 if thread_type == ThreadType.USER else 11
					}
					print(params)
					params['params'] = self._encode(params['params'])
					async def handle(start, end, params, chunk_id):
						while True:
							data = await self._fetch_chunk(session, file_url, start, end)
							# print(data)
							# print(type(data))
							if data == None:
								continue
							else:
								return await self._upload_chunk(sem, session, upload_url, params, data, file_name, chunk_id)
					tasks.append(handle(start, end, params, chunk_id))
				responses = await asyncio.gather(*tasks)
				# print(responses)
				failed_chunks = []
				last_success_resp = None
				for i, r in enumerate(responses):
					if isinstance(r, int):
						failed_chunks.append(r)
					else:
						last_success_resp = r
	
				if last_success_resp is None:
					raise ValueError("All initial chunk uploads failed.")
				
				last_data = self._decode(last_success_resp['data'])['data']
				final_client_id = last_data['clientFileId']
				if failed_chunks:
					retry_tasks = []
					for failed_id in failed_chunks:
						idx = failed_id - 1
						start = idx * chunk_size
						end = min(start + chunk_size - 1, fileSize - 1)
						params = {
							'zpw_ver': 655,
							'zpw_type': 24,
							'params': {
								'totalChunk': total_chunks,
								'fileName': file_name,
								'clientId': final_client_id,
								'totalSize': totalSize,
								'imei': self._imei,
								'chunkId': failed_id,
								id_field: thread_id
							},
							'type': 2 if thread_type == ThreadType.USER else 11
						}
						params['params'] = self._encode(params['params'])
						async def retry_handle(start, end, params, failed_id):
							data = await self._fetch_chunk(session, file_url, start, end)
							return await self._upload_chunk(sem, session, upload_url, params, data, file_name, failed_id)
						retry_tasks.append(retry_handle(start, end, params, failed_id))
					retry_responses = await asyncio.gather(*retry_tasks)
					still_failed = [r for r in retry_responses if isinstance(r, int)]
					if still_failed:
						raise ValueError(f"Some chunks still failed after retry: {still_failed}")
	
					for i, r in enumerate(retry_responses):
						if not isinstance(r, int) and failed_chunks[i] == total_chunks:
							last_success_resp = r
							last_data = self._decode(last_success_resp['data'])['data']
							final_client_id = last_data['clientFileId']
	
				params = {
					'zpw_ver': 655,
					'zpw_type': 24,
					'params': {
						'totalChunk': total_chunks,
						'fileName': file_name,
						'clientId': final_client_id,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': total_chunks,
						id_field: thread_id
					},
					'type': 2 if thread_type == ThreadType.USER else 11
				}
				params['params'] = self._encode(params['params'])
				zero_bio = BytesIO(b'')
				zero_bio.name = file_name
				data = aiohttp.FormData()
				data.add_field('chunkContent', zero_bio, filename=file_name)
				async with session.post(upload_url, params=params, data=data, headers=HEADERS, cookies=self._state.get_cookies()) as resp:
					resp.raise_for_status()
					final_text = await resp.text()
					final_resp = json.loads(final_text)
					decoded = self._decode(final_resp['data'])
					print(decoded)
					return decoded['data']['url']
		except Exception as e:
			traceback.print_exc()
			


	def UploadFileUrl(self, fileUrl, thread_id, thread_type, headers=None):
		"""Upload data to Zalo cloud trực tiếp từ URL mà không lưu file.

		Args:
			fileUrl (str): URL của file cần upload
			toid (int | str): ID người nhận (mặc định)
		Returns:
			str: URL của file đã upload lên Zalo
		"""
		head_resp = requests.get(fileUrl, allow_redirects=True, stream=True, headers=headers)
		fileSize = int(head_resp.headers.get('Content-Length', 0))
		max_chunk_size = 3145000
		if fileSize > 100000000:
			totalSize = max_chunk_size
		else:
			totalSize = fileSize
		clientId = _util.now()
		fileName = os.path.basename(urlparse(fileUrl).path) or 'upload.bin'
		if thread_type == ThreadType.USER:
			type = 2
			upload_url = "https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/upload"
		else:
			type = 11
			upload_url = "https://tt-files-wpa.chat.zalo.me/api/group/asyncfile/upload"
		if fileSize > max_chunk_size:
			with requests.get(fileUrl, stream=True, headers=headers, timeout=1) as resp:
				resp.raise_for_status()
				chunk_id = 1
				total_chunks = (fileSize + max_chunk_size - 1) // max_chunk_size
				last_response_data = None
				for chunk in resp.iter_content(chunk_size=max_chunk_size):
					if not chunk:
						break
					bio = BytesIO(chunk)
					bio.name = fileName

					params = {
						"zpw_ver": 655,
						"zpw_type": 24,
						"params": {
							'totalChunk': total_chunks,
							'fileName': fileName,
							'clientId': clientId,
							'totalSize': totalSize,
							'imei': self._imei,
							'chunkId': chunk_id
						},
						"type": type
					}
					if thread_type == ThreadType.USER:
						params["params"]["toid"] = thread_id
					else:
						params["params"]["grid"] = thread_id
					params["params"] = self._encode(params["params"])
					files = [("chunkContent", bio)]
					resp_upload = self._post(upload_url, params=params, files=files)
					data_value = json.loads(resp_upload.text).get("data")
					decoded = self._decode(data_value)
					print(decoded)
					last_response_data = decoded.get('data')
					chunk_id += 1
				client_file_id = last_response_data.get('clientFileId')
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': total_chunks,
						'fileName': fileName,
						'clientId': client_file_id,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': total_chunks
					},
					"type": type
				}
				if thread_type == ThreadType.USER:
					params["params"]["toid"] = thread_id
				else:
					params["params"]["grid"] = thread_id

				params["params"] = self._encode(params["params"])
				files = [("chunkContent", BytesIO(chunk))]
				files[0][1].name = fileName
				resp_final = self._post(upload_url, params=params, files=files)
				data_value = json.loads(resp_final.text).get("data")
				decoded = self._decode(data_value)
				print(decoded)
				return decoded.get('data', {}).get('url')
		else:
			resp_get = requests.get(fileUrl, stream=True, timeout=1)
			resp_get.raise_for_status()
			bio = BytesIO(resp_get.content)
			bio.name = fileName

			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': 1,
					'fileName': fileName,
					'clientId': _util.now(),
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': 1
				},
				"type": type
			}
			if thread_type == ThreadType.USER:
				params["params"]["toid"] = thread_id
			else:
				params["params"]["grid"] = thread_id
			params["params"] = self._encode(params["params"])
			files = [("chunkContent", bio)]
			resp_upload = self._post(upload_url, params=params, files=files)
			decoded = self._decode(json.loads(resp_upload.text).get("data"))
			print(decoded)
			data = decoded.get('data', {})
			if data.get('url'):
				return data['url']
			client_file_id = data.get('clientFileId')
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': 1,
					'fileName': fileName,
					'clientId': client_file_id,
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': 1
				},
				"type": type
			}
			if thread_type == ThreadType.USER:
				params["params"]["toid"] = thread_id
			else:
				params["params"]["grid"] = thread_id
			params["params"] = self._encode(params["params"])
			resp_retry = self._post(upload_url, params=params, files=files)
			decoded = self._decode(json.loads(resp_retry.text).get("data"))
			print(decoded)
			return decoded.get('data', {}).get('url')

	def cloudUploadBytes(self, file_bytes: bytes, filename: str, toid: str = '4546788742527218184') -> str:
		"""Upload data byte trực tiếp lên Zalo cloud mà không cần URL.

		Args:
			file_bytes (bytes): Dữ liệu nhị phân của file cần upload
			filename (str): Tên file kèm phần mở rộng
			toid (str): ID người nhận trên Zalo (mặc định)

		Returns:
			str: URL của file đã upload
		"""
		fileSize = len(file_bytes)
		max_chunk_size = 3145000  # 2MB
		if fileSize > 100000000:
			totalSize = max_chunk_size
		else:
			totalSize = fileSize
		
		clientId = _util.now()
		upload_url = "https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/upload"
		stream = BytesIO(file_bytes)
		stream.name = filename
		total_chunks = (fileSize + max_chunk_size - 1) // max_chunk_size
		last_response_data = None
		for chunk_id in range(1, total_chunks + 1):
			start = (chunk_id - 1) * max_chunk_size
			end = min(start + max_chunk_size, fileSize)
			stream.seek(start)
			chunk_data = stream.read(end - start)
			bio = BytesIO(chunk_data)
			bio.name = filename
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': total_chunks,
					'fileName': filename,
					'clientId': clientId,
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': chunk_id,
					'toid': toid
				},
				"type": 2
			}
			params["params"] = self._encode(params["params"])
			files = [("chunkContent", bio)]
			resp = self._post(upload_url, params=params, files=files)
			decoded = self._decode(json.loads(resp.text).get("data"))
			last_response_data = decoded.get('data')
			if chunk_id == total_chunks:
				clientId = last_response_data.get('clientFileId', clientId)
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': total_chunks,
						'fileName': filename,
						'clientId': clientId,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': chunk_id,
						'toid': toid
					},
					"type": 2
				}
				params["params"] = self._encode(params["params"])
				files = [("chunkContent", bio)]
				resp = self._post(upload_url, params=params, files=files)
				decoded = self._decode(json.loads(resp.text).get("data"))
				last_response_data = decoded.get('data')
			print(last_response_data)


		return last_response_data.get('url')


	def settingUpdate(self, add_friend_via_phone=None, add_friend_via_qr=None, add_friend_via_group=None, add_friend_via_contact=None, display_on_recommend_friend=None, accept_stranger_call=None, receive_message=None, call=None):
		"""Set Mode Friend Allow
			1 = True, 2 = False
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"params": {}
		}
		if add_friend_via_qr:
			params["params"]["add_friend_via_qr"] = add_friend_via_qr
		elif display_on_recommend_friend:
			params["params"]["display_on_recommend_friend"] = display_on_recommend_friend
		elif add_friend_via_group:
			params["params"]["add_friend_via_group"] = add_friend_via_group
		elif add_friend_via_contact:
			params["params"]["add_friend_via_contact"] = add_friend_via_contact
		elif add_friend_via_phone:
			params["params"]["add_friend_via_phone"] = add_friend_via_phone
		elif receive_message:
			params["params"]["receive_message"] = receive_message
		elif accept_stranger_call:
			params["params"]["accept_stranger_call"] = accept_stranger_call
		elif call:
			params["params"]["enable_call"] = call
		else:
			return "dit me may"

		params["params"] = self._encode(params["params"])
		response = self._get("https://wpa.chat.zalo.me/api/setting/update", params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 3006, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 3006, "error_message": results}

			return results


	def cloudUploadv2(self, fileBytes, fileName, toid='4546788742527218184'):
		"""Upload data to zalo cloud.

		Args:
			filePath (str): A path to the image to upload/change avatar
			toid (int | str | None): idk

		Returns:
			url
		"""
		fileSize = len(fileBytes)
		max_chunk_size = 3145000
		if fileSize > 100000000:
			totalSize = max_chunk_size
		else:
			totalSize = fileSize
		
		clientId = _util.now()
		if fileSize > max_chunk_size:
			chunk_id = 1
			while chunk := fileBytes[:max_chunk_size]:
				if not chunk:
					break

				chunk_file = BytesIO(chunk)
				chunk_file.name = fileName
				files = [("chunkContent", chunk_file)]
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': (fileSize // max_chunk_size) + 1,
						'fileName': fileName,
						'clientId': clientId,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': chunk_id,
						'toid': toid
					},
					"type": 2
				}
				params["params"] = self._encode(params["params"])
				url = "https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/upload"
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(data_value)
				chunk_id += 1

			data = cc.get('data')
			
			client_file_id = data.get('clientFileId')
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': (fileSize // max_chunk_size) + 1,
					'fileName': fileName,
					'clientId': client_file_id,
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': (fileSize // max_chunk_size) + 1,
					'toid': toid
				},
				"type": 2
			}
			params["params"] = self._encode(params["params"])
			response = self._post(url, params=params, files=files)
			json_data = json.loads(response.text)
			data_value = json_data.get("data")
			cc = self._decode(data_value)
			data = cc.get('data')
			return data.get('url')
		else:
			files = [("chunkContent", fileBytes)]
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': 1,
					'fileName': fileName,
					'clientId': _util.now(),
					'totalSize': totalSize,
					'imei': self._imei,
					'chunkId': 1,
					'toid': toid
				},
				"type": 2
			}
			params["params"] = self._encode(params["params"])
			url = "https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/upload"
			response = self._post(url, params=params, files=files)
			json_data = json.loads(response.text)
			data_value = json_data.get("data")
			cc = self._decode(data_value)
			data = cc.get('data')
			if data.get('url'):
				return data.get('url')
			else:
				client_file_id = data.get('clientFileId')
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': 1,
						'fileName': fileName,
						'clientId': client_file_id,
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': 1,
						'toid': toid
					},
					"type": 2
				}
				params["params"] = self._encode(params["params"])
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(params=data_value)
				data = cc.get('data')
				return data.get('url')


	def upload_voice(self, filePath, thread_id='4546788742527218184', thread_type=ThreadType.GROUP):
		"""Upload voice to zalo cloud.

		Args:
			filePath (str): A path to the image to upload/change avatar
			thread_id (int | str | None): idk

		Returns:
			url
		"""
		if thread_type == ThreadType.USER:
			fileSize = os.path.getsize(filePath)
			max_chunk_size = 3145000
			if fileSize > 100000000:
				totalSize = max_chunk_size
			else:
				totalSize = fileSize
		
			clientId = _util.now()
			if fileSize > max_chunk_size:
				with open(filePath, 'rb') as f:
					chunk_id = 1
					while chunk := f.read(max_chunk_size):
						if not chunk:
							break

						chunk_file = BytesIO(chunk)
						fileName = os.path.basename(filePath)
						chunk_file.name = fileName
						files = [("chunkContent", chunk_file)]
						params = {
							"zpw_ver": 655,
							"zpw_type": 24,
							"params": {
								'totalChunk': (fileSize // max_chunk_size) + 1,
								'fileName': fileName,
								'clientId': clientId,
								'totalSize': totalSize,
								'imei': self._imei,
								'chunkId': chunk_id,
								'toid': thread_id,
								'fileType': 'aac'
							},
							"type": 2
						}
						params["params"] = self._encode(params["params"])
						url = "https://tt-files-wpa.chat.zalo.me/api/message/voice/upload"
						response = self._post(url, params=params, files=files)
						json_data = json.loads(response.text)
						data_value = json_data.get("data")
						cc = self._decode(data_value)
						chunk_id += 1

					data = cc.get('data')
					client_file_id = data.get('clientFileId')
					params = {
						"zpw_ver": 655,
						"zpw_type": 24,
						"params": {
							'totalChunk': (fileSize // max_chunk_size) + 1,
							'fileName': fileName,
							'clientId': client_file_id,
							'totalSize': totalSize,
							'imei': self._imei,
							'chunkId': (fileSize // max_chunk_size) + 1,
							'toid': thread_id,
							'fileType': 'aac'
						},
						"type": 2
					}
					params["params"] = self._encode(params["params"])
					response = self._post(url, params=params, files=files)
					json_data = json.loads(response.text)
					data_value = json_data.get("data")
					cc = self._decode(data_value)
					data = cc.get('data')
					return data.get('url')
			else:
				files = [("chunkContent", open(filePath, "rb"))]
				fileName = os.path.basename(filePath)
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': 1,
						'fileName': fileName,
						'clientId': _util.now(),
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': 1,
						'toid': thread_id,
						'fileType': 'aac'
					},
					"type": 2
				}
				params["params"] = self._encode(params["params"])
				url = "https://tt-files-wpa.chat.zalo.me/api/message/voice/upload"
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(data_value)
				data = cc.get('data')
				if data.get('url'):
					return data.get('url')
				else:
					client_file_id = data.get('clientFileId')
					params = {
						"zpw_ver": 655,
						"zpw_type": 24,
						"params": {
							'totalChunk': 1,
							'fileName': fileName,
							'clientId': client_file_id,
							'totalSize': totalSize,
							'imei': self._imei,
							'chunkId': 1,
							'toid': thread_id,
							'fileType': 'aac'
						},
						"type": 2
					}
					params["params"] = self._encode(params["params"])
					response = self._post(url, params=params, files=files)
					json_data = json.loads(response.text)
					data_value = json_data.get("data")
					cc = self._decode(params=data_value)
					data = cc.get('data')
					return data.get('url')
		elif thread_type == ThreadType.GROUP:
			fileSize = os.path.getsize(filePath)
			max_chunk_size = 3145000
			if fileSize > 100000000:
				totalSize = max_chunk_size
			else:
				totalSize = fileSize
		
			clientId = _util.now()
			if fileSize > max_chunk_size:
				with open(filePath, 'rb') as f:
					chunk_id = 1
					while chunk := f.read(max_chunk_size):
						if not chunk:
							break

						chunk_file = BytesIO(chunk)
						fileName = os.path.basename(filePath)
						chunk_file.name = fileName
						files = [("chunkContent", chunk_file)]
						params = {
							"zpw_ver": 655,
							"zpw_type": 24,
							"params": {
								'totalChunk': (fileSize // max_chunk_size) + 1,
								'fileName': fileName,
								'clientId': clientId,
								'totalSize': totalSize,
								'imei': self._imei,
								'chunkId': chunk_id,
								'grid': thread_id,
								'fileType': 'aac'
							},
							"type": 11
						}
						params["params"] = self._encode(params["params"])
						url = "https://tt-files-wpa.chat.zalo.me/api/group/voice/upload"
						response = self._post(url, params=params, files=files)
						json_data = json.loads(response.text)
						data_value = json_data.get("data")
						cc = self._decode(data_value)
						chunk_id += 1

					data = cc.get('data')
					client_file_id = data.get('clientFileId')
					params = {
						"zpw_ver": 655,
						"zpw_type": 24,
						"params": {
							'totalChunk': (fileSize // max_chunk_size) + 1,
							'fileName': fileName,
							'clientId': client_file_id,
							'totalSize': totalSize,
							'imei': self._imei,
							'chunkId': (fileSize // max_chunk_size) + 1,
							'grid': thread_id,
							'fileType': 'aac'
						},
						"type": 11
					}
					params["params"] = self._encode(params["params"])
					response = self._post(url, params=params, files=files)
					json_data = json.loads(response.text)
					data_value = json_data.get("data")
					cc = self._decode(data_value)
					data = cc.get('data')
					return data.get('url')
			else:
				files = [("chunkContent", open(filePath, "rb"))]
				fileName = os.path.basename(filePath)
				params = {
					"zpw_ver": 655,
					"zpw_type": 24,
					"params": {
						'totalChunk': 1,
						'fileName': fileName,
						'clientId': _util.now(),
						'totalSize': totalSize,
						'imei': self._imei,
						'chunkId': 1,
						'grid': thread_id,
						'fileType': 'aac'
					},
					"type": 11
				}
				params["params"] = self._encode(params["params"])
				url = "https://tt-files-wpa.chat.zalo.me/api/group/voice/upload"
				response = self._post(url, params=params, files=files)
				json_data = json.loads(response.text)
				data_value = json_data.get("data")
				cc = self._decode(data_value)
				data = cc.get('data')
				if data.get('url'):
					return data.get('url')
				else:
					client_file_id = data.get('clientFileId')
					params = {
						"zpw_ver": 655,
						"zpw_type": 24,
						"params": {
							'totalChunk': 1,
							'fileName': fileName,
							'clientId': client_file_id,
							'totalSize': totalSize,
							'imei': self._imei,
							'chunkId': 1,
							'grid': thread_id,
							'fileType': 'aac'
						},
						"type": 11
					}
					params["params"] = self._encode(params["params"])
					response = self._post(url, params=params, files=files)
					json_data = json.loads(response.text)
					data_value = json_data.get("data")
					cc = self._decode(params=data_value)
					data = cc.get('data')
					return data.get('url')

	def upload_voice_url(self, url, thread_id='4546788742527218184', thread_type=ThreadType.GROUP):
		"""Upload voice to Zalo cloud using a URL.

		Args:
			url (str): URL of the voice file to upload
			thread_id (int | str | None): Target thread ID
			thread_type (ThreadType): Type of thread (USER or GROUP)

		Returns:
			str: URL of the uploaded voice
		"""
		response = requests.get(url)
		if response.status_code != 200:
			raise Exception(f"Failed to download file from {url}")
		file_data = BytesIO(response.content)
		file_data.name = os.path.basename(url)
		file_size = len(response.content)
		max_chunk_size = 3145000
		client_id = _util.now()
		if thread_type == ThreadType.USER:
			upload_url = "https://tt-files-wpa.chat.zalo.me/api/message/voice/upload"
			thread_key = 'toid'
			upload_type = 2
		elif thread_type == ThreadType.GROUP:
			upload_url = "https://tt-files-wpa.chat.zalo.me/api/group/voice/upload"
			thread_key = 'grid'
			upload_type = 11
		else:
			raise ValueError("Invalid thread_type")
		def upload_chunk(chunk, chunk_id, total_chunks, client_file_id=None):
			if client_file_id is None:
				client_file_id = client_id
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": {
					'totalChunk': total_chunks,
					'fileName': file_data.name,
					'clientId': client_file_id,
					'totalSize': max_chunk_size,
					'imei': self._imei,
					'chunkId': chunk_id,
					thread_key: thread_id,
					'fileType': 'aac'
				},
				"type": upload_type
			}
			params["params"] = self._encode(params["params"])
			files = [("chunkContent", chunk)]
			response = self._post(upload_url, params=params, files=files)
			json_data = json.loads(response.text)
			data_value = json_data.get("data")
			return self._decode(data_value)

		if file_size > max_chunk_size:
			total_chunks = (file_size // max_chunk_size) + 1
			chunk_id = 1
			while chunk := file_data.read(max_chunk_size):
				if not chunk:
					break
				chunk_file = BytesIO(chunk)
				chunk_file.name = file_data.name
				cc = upload_chunk(chunk_file, chunk_id, total_chunks)
				chunk_id += 1
			data = cc.get('data')
			client_file_id = data.get('clientFileId')
			cc = upload_chunk(BytesIO(b''), total_chunks, total_chunks, client_file_id)
			data = cc.get('data')
			return data.get('url') + ".aac"
		else:
			cc = upload_chunk(file_data, 1, 1)
			data = cc.get('data')
			if data.get('url'):
				return data.get('url') + ".aac"
			else:
				client_file_id = data.get('clientFileId')
				cc = upload_chunk(file_data, 1, 1, client_file_id)
				data = cc.get('data')
				return data.get('url') + ".aac"

	def renewLink(self, url, thread_id, thread_type, fileSize=None, fileName=None, fileChecksum=None, extension=None):
		"""Undo message from the client by ID.

		Args:
			msgId (int | str): Message ID to undo
			cliMsgId (int | str): Client Msg ID to undo
			thread_id (int | str): User/Group ID to undo message
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` undo message status
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		response = requests.get(url)
		if response.status_code != 200:
			if fileSize == None:
				fileSize = 8851357
			if fileName == None:
				fileName = "callzl.har"
			if fileChecksum == None:
				fileChecksum = "3ffa6fb755ce2069ddae8a37b574c3ad"
			if extension == None:
				extension = "txt"
		else:
			if fileSize == None:
				fileSize = len(response.content)
			if fileName == None:
				fileName = os.path.basename(url)
			if fileChecksum == None:
				fileChecksum = hashlib.md5(response.content).hexdigest()
			if extension == None:
				extension = "txt"

		# print(fileSize)
		# print(fileName)
		# print(fileChecksum)
		# print(extension)
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				'toId': thread_id, 
				'msgType': 1, 
				'msgInfo': json.dumps({
					"url": url, 
					"fileName": fileName, 
					"extension": extension, 
					"checksum": fileChecksum, 
					"size": str(fileSize), 
					"fType": 1, 
					"isE2EE": 0, 
					"isOriginal": 1
				}), 
				'clientId': str(_util.now()), 
				'imei': self._imei, 
				'isGroup': 0
			} 
		}
		payload["params"] = self._encode(payload["params"])

		response = self._post("https://tt-files-wpa.chat.zalo.me/api/message/renewlink", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}
					
			# print(results)
			if "async" in results and results.get("async") == True:
				results = self.fileEvent.get()
				if results.get("originalUrl") == url and results.get("destId") == str(thread_id):
					results = {"fileUrl": results.get("renewUrl")}
			return (Group.fromDict(results, None))

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")


	def undoMessage(self, msgId, cliMsgId, thread_id, thread_type):
		"""Undo message from the client by ID.

		Args:
			msgId (int | str): Message ID to undo
			cliMsgId (int | str): Client Msg ID to undo
			thread_id (int | str): User/Group ID to undo message
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` undo message status
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"msgId": str(msgId),
				"cliMsgIdUndo": str(cliMsgId),
				"clientId": str(_util.now())
			} 
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-chat3-wpa.chat.zalo.me/api/message/undo"
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/undomsg"
			payload["params"]["grid"] = str(thread_id)
			payload["params"]["visibility"] = 3
			payload["params"]["imei"] = self._imei
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def undoReqFriend(self, uid):
		"""Undo message from the client by ID.

		Args:
			msgId (int | str): Message ID to undo
			cliMsgId (int | str): Client Msg ID to undo
			thread_id (int | str): User/Group ID to undo message
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` undo message status
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				'fid': str(uid)
			}
		}

		payload["params"] = self._encode(payload["params"])
		response = self._post("https://tt-friend-wpa.chat.zalo.me/api/friend/undo", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def archivedChat(self, groupId):
		"""Undo message from the client by ID.

		Args:
			msgId (int | str): Message ID to undo
			cliMsgId (int | str): Client Msg ID to undo
			thread_id (int | str): User/Group ID to undo message
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` undo message status
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				'ids': [{'id': groupId, 'type': 1}], 
				'version': 1759751648000, 
				'actionType': 0, 
				'imei': self._imei
			}
		}

		payload["params"] = self._encode(payload["params"])
		response = self._post("https://label-wpa.chat.zalo.me/api/archivedchat/update", params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return Group.fromDict(results, None)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def multiUndoMessage(self, undoObj, thread_id, thread_type):
		"""Undo message from the client by ID.

		Args:
			undoObj (list): Data for undo demo ([{'cliMsgId': 1740106411307, 'cliMsgIdUndo': '1740106271572', 'globalMsgId': '6337909507597'}, {'cliMsgId': 1740106411308, 'cliMsgIdUndo': '1740106270914', 'globalMsgId': '6337909476291'}, {'cliMsgId': 1740106411309, 'cliMsgIdUndo': '1740106270226', 'globalMsgId': '6337909428483'}, {'cliMsgId': 1740106411310, 'cliMsgIdUndo': '1740106269496', 'globalMsgId': '6337909383547'}, {'cliMsgId': 1740106411311, 'cliMsgIdUndo': '1740106268753', 'globalMsgId': '6337909336491'}, {'cliMsgId': 1740106411312, 'cliMsgIdUndo': '1740106268000', 'globalMsgId': '6337909292380'}])
			thread_id (int | str): User/Group ID to undo message
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` undo message status
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": { 
				'msgs': undoObj
			} 
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-chat3-wpa.chat.zalo.me/api/message/undo"
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/multi_undomsg"
			payload["params"]["grid"] = str(thread_id)

		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendReaction(self, messageObject, reactionIcon, thread_id, thread_type, reactionType=75, ttl=0):
		"""Reaction message by ID.

		Args:
			messageObject (Message): Message Object to reaction
			reactionIcon (str): Icon/Text to reaction
			thread_id (int | str): Group/User ID contain message to reaction
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` message reaction data
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"react_list": [{
					"message": json.dumps({
						"rMsg": [{
							"gMsgID": int(messageObject.msgId),
							"cMsgID": int(messageObject.cliMsgId),
							"msgType": _util.getClientMessageType(messageObject.msgType)
						}],
						"rIcon": reactionIcon,
						"rType": reactionType,
						# "rIcon": "cc",
						# "rType": "70",

						"source": 6
					}),
					"clientId": _util.now() + 3000
				}],
				"imei": self._imei,
				"ttl": ttl
			}
		}

		if thread_type == ThreadType.USER:
			url = "https://reaction.chat.zalo.me/api/message/reaction"
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://reaction.chat.zalo.me/api/group/reaction"
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendMultiReaction(self, reactionObj, reactionIcon, thread_id, thread_type, reactionType=75, ttl=0):
		"""Reaction message by ID.

		Args:
			reactionObj (MessageReaction): Message(s) data to reaction
			reactionIcon (str): Icon/Text to reaction
			thread_id (int | str): Group/User ID contain message to reaction
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` message reaction data
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"react_list": [{
					"message": {
						"rMsg": [],
						"rIcon": reactionIcon,
						"rType": reactionType,
						"source": 6
					},
					"clientId": _util.now() + 3000
				}],
				"imei": self._imei,
				"ttl": ttl
			}
		}

		if isinstance(reactionObj, dict):
			payload["params"]["react_list"][0]["message"]["rMsg"].append(reactionObj)
		elif isinstance(reactionObj, list):
			payload["params"]["react_list"][0]["message"]["rMsg"] = reactionObj
		else:
			raise ZaloUserError("Reaction type is invalid")

		if thread_type == ThreadType.USER:
			url = "https://reaction.chat.zalo.me/api/message/reaction"
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://reaction.chat.zalo.me/api/group/reaction"
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"]["react_list"][0]["message"] = json.dumps(payload["params"]["react_list"][0]["message"])
		# print(payload)
		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendRemoteFile(self, fileUrl, thread_id, thread_type, fileName="cc", fileSize=None, extension="tahn", fileChecksum=None, ttl=0):
		"""Send File to a User/Group with url.

		Args:
			fileUrl (str): File url to send
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP
			fileName (str): File name to send
			fileSize (int): File size to send
			extension (str): type of file to send (py, txt, mp4, ...)

		Returns:
			object: `User/Group` (Returns msg ID just sent)
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		if fileSize == None:
			headers = {
				"Connection": "keep-alive",
				"sec-ch-ua": "\"Not)A;Brand\";v\u003d\"8\", \"Chromium\";v\u003d\"138\", \"Google Chrome\";v\u003d\"138\"",
				"sec-ch-ua-mobile": "?1",
				"sec-ch-ua-platform": "\"Android\"",
				"Upgrade-Insecure-Requests": "1",
				"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36",
				"Accept": "text/html,application/xhtml+xml,application/xml;q\u003d0.9,image/avif,image/webp,image/apng,*/*;q\u003d0.8,application/signed-exchange;v\u003db3;q\u003d0.7",
				"Sec-Fetch-Site": "same-origin",
				"Sec-Fetch-Mode": "navigate",
				"Sec-Fetch-User": "?1",
				"Sec-Fetch-Dest": "document",
				"Accept-Encoding": "gzip, deflate, br, zstd",
				"Accept-Language": "en-US,en;q\u003d0.9,vi;q\u003d0.8,pt;q\u003d0.7"
			}
			try:
				fileSize = int(requests.get(fileUrl, stream=True, headers=headers).headers.get("Content-Length"))
			except Exception:
				try:
					fileSize = int(requests.get(fileUrl, stream=True).headers.get("Content-Length"))
				except Exception:
					try:
						fileSize = int(requests.head(fileUrl, stream=True, headers=headers).headers.get("Content-Length"))
					except Exception:
						try:
							fileSize = int(requests.head(fileUrl, stream=True).headers.get("Content-Length"))
						except Exception:
							fileSize = 0
			# print(fileSize)
			fileName = os.path.basename(fileUrl)
			extension = os.path.splitext(fileName)[1].replace(".", "").strip()
		if not fileChecksum:
			fileChecksum = asyncio.run(getMd5FileUrl(fileUrl))

		# has_extension = fileName.rsplit(".")
		# extension = has_extension[-1:][0] if len(has_extension) >= 2 else extension
		print(fileSize)
		

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"fileId": str(int(_util.now() + 3000 * 2)),
				"checksum": fileChecksum,
				"checksumSha": "",
				"extension": extension,
				"totalSize": fileSize,
				"fileName": fileName,
				"clientId": _util.now() + 3000,
				"fType": 1,
				"fileCount": 0,
				"fdata": "{}",
				"fileUrl": fileUrl,
				"zsource": 401,
				"ttl": ttl
			}
		}

		if thread_type == ThreadType.GROUP:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/asyncfile/msg"
			payload["params"]["grid"] = str(thread_id)
		else:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/asyncfile/msg"
			payload["params"]["toid"] = str(thread_id)
			payload["params"]["imei"] = self._imei
		# else:
			

		payload["params"] = self._encode(payload["params"])
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendRemoteVideo(self, videoUrl, thumbnailUrl, duration, thread_id, thread_type, width=1280, height=720, message=None, ttl=0, fileSize=0, forward=None):
		"""Send (Forward) video to a User/Group with url.

		Warning:
			This is a feature created through the forward function.
			Because Zalo Web does not allow viewing videos.

		Args:
			videoUrl (str): Video link to send
			thumbnailUrl (str): Thumbnail link for video
			duration (int | str): Time for video (ms)
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP
			width (int): Width of the video
			height (int): Height of the video
			message (Message): Message to send with video

		Returns:
			object: `User/Group` (Returns msg ID just sent)
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}
		if forward:
			id = uuid.uuid4().hex
			reference = json.dumps({
				"type": 3,
				"data": json.dumps({
					"id": id,
					"ts": _util.now(),
					"logSrcType": 2,
					"fwLvl": 1,
					"rootMsgRef": {
						"id": id,
						"ts": _util.now(),
						"logSrcType": 2
					},
					"cmi": forward["cliMsg"],
					"gmi": forward["msgId"],
					"srcId": forward["srcId"],
					"tId": forward["tId"],
					"srcType": 6
				})
			})
		else:
			reference = None
		payload = {
			"params": {
				"clientId": str(_util.now() + 3000),
				"ttl": ttl,
				"zsource": 704,
				"msgType": 5,
				"msgInfo": json.dumps({
					"videoUrl": str(videoUrl),
					"thumbUrl": str(thumbnailUrl),
					"duration": int(duration),
					"width": int(width),
					"height": int(height),
					"fileSize": int(fileSize),
					"properties": None,
					"title": message.text or "" if message else "",
					"videoRotation": 0,
					"reference": reference
				})
			}
		}
		"""
		{
			'toId': '1126676322889155169', 
			'imei': 'Tahn', 
			'ttl': 0, 
			'zsource': 704, 
			'msgType': 5, 
			'clientId': '1764145233068', 
			'msgInfo': json.dumps({
				"videoUrl":"https://f143-zvc.dlmd.me/mc/fc605296a422497c1033/228301052555397930", 
				"thumbUrl":"https://f16-zpcloud.zdn.vn/5272535860518190599/2f208812b18b3dd5649a.jpg",
				"duration":12000, 
				"width":576, 
				"height":768, 
				"fileSize": 1085631,
				"properties":None,
				"title":"",
				"videoRotation":0,
				"reference": json.dumps({
					"type":3,
					"data":json.dumps({
						"id":"f84eef93c36ba9298efdbbd00f2b2900",
						"ts":1764145088493,
						"logSrcType":2,
						"fwLvl":1,
						"rootMsgRef": {
							"id":"f84eef93c36ba9298efdbbd00f2b2900",
							"ts":1764145088493,
							"logSrcType":2
						},
						"cmi":1764145087371,
						"gmi":"7264554814367",
						"srcId":"136747183864777510",
						"tId":"4986496986921605616",
						"srcType":6
					})
				})
			}), 
			'decorLog': json.dumps({
				"fw":{
					"pmsg":{
						"st":2,
						"ts":1764145088493,
						"id":"f84eef93c36ba9298efdbbd00f2b2900"
					},
					"rmsg":{
						"st":2,
						"ts":1764145088493,
						"id":"f84eef93c36ba9298efdbbd00f2b2900"
					},
					"fwLvl":1
				}
			})
		}"""

		print(payload)
		if message and message.mention:
			payload["params"]["mentionInfo"] = message.mention

		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/forward"
			payload["params"]["toId"] = str(thread_id)
			payload["params"]["imei"] = self._imei
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/forward"
			payload["params"]["visibility"] = 0
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendRemoteVoice(self, voiceUrl, thread_id, thread_type, fileSize=None, ttl=0):
		"""Send voice by url.

		Args:
			voiceUrl (str): Voice link to send
			thread_id (int | str): User/Group ID to change status in
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP
			fileSize (int | str): Voice content length (size) to send

		Returns:
			object: `User/Group` response

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		headers = {
			"Connection": "keep-alive",
			"sec-ch-ua": "\"Not)A;Brand\";v\u003d\"8\", \"Chromium\";v\u003d\"138\", \"Google Chrome\";v\u003d\"138\"",
			"sec-ch-ua-mobile": "?1",
			"sec-ch-ua-platform": "\"Android\"",
			"Upgrade-Insecure-Requests": "1",
			"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36",
			"Accept": "text/html,application/xhtml+xml,application/xml;q\u003d0.9,image/avif,image/webp,image/apng,*/*;q\u003d0.8,application/signed-exchange;v\u003db3;q\u003d0.7",
			"Sec-Fetch-Site": "same-origin",
			"Sec-Fetch-Mode": "navigate",
			"Sec-Fetch-User": "?1",
			"Sec-Fetch-Dest": "document",
			"Accept-Encoding": "gzip, deflate, br, zstd",
			"Accept-Language": "en-US,en;q\u003d0.9,vi;q\u003d0.8,pt;q\u003d0.7"
		}
		try:
			fileSize = int(requests.get(fileUrl, stream=True, headers=headers).headers.get("Content-Length"))
		except Exception:
			try:
				fileSize = int(requests.get(fileUrl, stream=True).headers.get("Content-Length"))
			except Exception:
				try:
					fileSize = int(requests.head(fileUrl, stream=True, headers=headers).headers.get("Content-Length"))
				except Exception:
					try:
						fileSize = int(requests.head(fileUrl, stream=True).headers.get("Content-Length"))
					except Exception:
						fileSize = 0
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"ttl": ttl,
				"zsource": -1,
				"msgType": 3,
				"clientId": str(_util.now() + 3000),
				"msgInfo": json.dumps({
					"voiceUrl": str(voiceUrl),
					"m4aUrl": str(voiceUrl),
					"fileSize": int(fileSize)
				})
			}
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/forward"
			payload["params"]["toId"] = str(thread_id)
			payload["params"]["imei"] = self._imei
		else:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/forward"
			payload["params"]["visibility"] = 0
			payload["params"]["grid"] = str(thread_id)

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendLocalImage(self, imagePath, thread_id, thread_type, width=2560, height=2560, message=None, custom_payload=None, ttl=0):
		"""Send Image to a User/Group with local file.

		Args:
			imagePath (str): Image directory to send
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP
			width (int): Image width to send
			height (int): Image height to send
			message (Message): Message to send with image

		Returns:
			object: `User/Group` objects response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		if custom_payload:
			if thread_type == ThreadType.USER:
				url = "https://tt-files-wpa.chat.zalo.me/api/message/photo_original/send"
			elif thread_type == ThreadType.GROUP:
				url = "https://tt-files-wpa.chat.zalo.me/api/group/photo_original/send"
			else:
				raise ZaloUserError("Thread type is invalid")

			payload = custom_payload

		else:
			uploadImage = self._uploadImage(imagePath, thread_id, thread_type)

			payload = {
				"params": {
					"photoId": uploadImage.get("photoId", int(_util.now() * 2)),
					"clientId": uploadImage.get("clientFileId", int(_util.now() - 1000)),
					"desc": message.text if message else "" or "",
					"width": width,
					"height": height,
					"rawUrl": uploadImage["normalUrl"],
					"thumbUrl": uploadImage["thumbUrl"],
					"hdUrl": uploadImage["hdUrl"],
					"thumbSize": "53932",
					"fileSize": "247671",
					"hdSize": "344622",
					"zsource": -1,
					"jcp": json.dumps({"sendSource": 1, "convertible": "jxl"}),
					"ttl": ttl,
					"imei": self._imei
				}
			}

			if message and message.mention:
				payload["params"]["mentionInfo"] = message.mention

			if thread_type == ThreadType.USER:
				url = "https://tt-files-wpa.chat.zalo.me/api/message/photo_original/send"
				payload["params"]["toid"] = str(thread_id)
				payload["params"]["normalUrl"] = uploadImage["normalUrl"]
			elif thread_type == ThreadType.GROUP:
				url = "https://tt-files-wpa.chat.zalo.me/api/group/photo_original/send"
				payload["params"]["grid"] = str(thread_id)
				payload["params"]["oriUrl"] = uploadImage["normalUrl"]
			else:
				raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			# print(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def joinbox(self, link):
		"""Join group by link.

			Args:
				link (str): Link to join

			Returns:
				object: `User/Group` objects
				dict: A dictionary containing error responses

			Raises:
				ZaloAPIException: If request failed

		"""
		if "zaloapp.com/qr/g/" in link:
			link = "https://zalo.me/g/" + link.replace("https://zaloapp.com/qr/g/", "").strip().replace("?src=qr", "").strip()
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}
		url = "https://tt-group-wpa.chat.zalo.me/api/group/link/join"
		payload = {
			"params": {
				'link': str(link), 
				'clientLang': 'vi'
			}
		}
		payload["params"] = self._encode(payload["params"])
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 3006, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 3006, "error_message": results}

			return Group.fromDict(results, None)

	def Inv(self, grid, allow=True, block=False):
		"""Join group by thread id

		Args:
			id (str): thread id to join

		Returns:
			object: `User/Group` objects
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed

		"""
		if block:
			isBlock = 1
		else:
			isBlock = 0
		if allow:
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": self._encode({
					'grid': str(grid), 
					'lang': 'vi'
				})
			}
			url = "https://tt-group-wpa.chat.zalo.me/api/group/inv-box/join"
		else:
			params = {
				"zpw_ver": 655,
				"zpw_type": 24,
				"params": self._encode({
					'invitations': json.dumps([
						{
							"grid": str(grid)
						}
					]), 
					'block': isBlock
				})
			}
			url = "https://tt-group-wpa.chat.zalo.me/api/group/inv-box/mdel-inv"

		response = self._get(url, params=params)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 3006, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 3006, "error_message": results}

		return Group.fromDict(results, None)

	def sendMultiLocalImage(self, imagePathList, thread_id, thread_type, width=2560, height=2560, message=None, ttl=0):
		"""Send Multiple Image to a User/Group with local file.

		Args:
			imagePathList (list): List image directory to send
			width (int): Image width to send
			height (int): Image height to send
			message (Message): Message to send with image
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` objects
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		uploadData = []

		if not isinstance(imagePathList, list) or len(imagePathList) < 1:
			raise ZaloUserError("image path must be a list to be able to send multiple at once.")

		groupLayoutId = str(_util.now())

		for i, imagePath in enumerate(imagePathList):
			uploadImage = self._uploadImage(imagePath, thread_id, thread_type)

			payload = {
				"params": {
					"photoId": uploadImage.get("photoId", int(_util.now() * 2)),
					"clientId": uploadImage.get("clientFileId", int(_util.now() - 1000)),
					"desc": message.text if message else "" or "",
					"width": width,
					"height": height,
					"groupLayoutId": groupLayoutId,
					"totalItemInGroup": len(imagePathList),
					"isGroupLayout": 1,
					"idInGroup": i,
					"rawUrl": uploadImage["normalUrl"],
					"thumbUrl": uploadImage["thumbUrl"],
					"hdUrl": uploadImage["hdUrl"],
					"thumbSize": "53932",
					"fileSize": "247671",
					"hdSize": "344622",
					"zsource": -1,
					"jcp": json.dumps({"sendSource": 1, "convertible": "jxl"}),
					"ttl": ttl,
					"imei": self._imei
				}
			}

			if message and message.mention:
				payload["params"]["mentionInfo"] = message.mention

			if thread_type == ThreadType.USER:
				payload["params"]["toid"] = str(thread_id)
				payload["params"]["normalUrl"] = uploadImage["normalUrl"]
			elif thread_type == ThreadType.GROUP:
				payload["params"]["grid"] = str(thread_id)
				payload["params"]["oriUrl"] = uploadImage["normalUrl"]
			else:
				raise ZaloUserError("Thread type is invalid")

			data = self.sendLocalImage(imagePath, thread_id, thread_type, width, height, message, custom_payload=payload)
			uploadData.append(data.toDict())

		return (
			Group.fromDict(uploadData, None) 
			if thread_type == ThreadType.GROUP else 
			User.fromDict(uploadData, None)
		)

	def sendLocalGif(self, gifPath, thumbnailUrl, thread_id, thread_type, gifName="vrxx.gif", width=500, height=500, ttl=0):
		"""Send Gif to a User/Group with local file.

		Args:
			gifPath (str): Gif path to send
			thumbnailUrl (str): Thumbnail of gif to send
			gifName (str): Gif name to send
			width (int): Gif width to send
			height (int): Gif height to send
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` objects
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		if not os.path.exists(gifPath):
			raise ZaloUserError(f"{gifPath} not found")

		files = [("chunkContent", open(gifPath, "rb"))]
		gifSize = len(open(gifPath, "rb").read())
		gifName = gifName if gifName else gifPath if "/" not in gifPath else gifPath.rstrip("/")[1]
		fileChecksum = hashlib.md5(open(gifPath, "rb").read()).hexdigest()

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"type": 1,
			"params": {
				"clientId": str(_util.now() + 3000),
				"fileName": gifName,
				"totalSize": gifSize,
				"width": width,
				"height": height,
				"msg": "",
				"type": 1,
				"ttl": ttl,
				"thumb": thumbnailUrl,
				"checksum": fileChecksum,
				"totalChunk": 1,
				"chunkId": 1
			}
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/gif"
			params["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/gif"
			params["params"]["visibility"] = 0
			params["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		params["params"] = self._encode(params["params"])

		response = self._post(url, params=params, files=files)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendSticker(self, stickerType, stickerId, cateId, thread_id, thread_type, ttl=0, reply=None):
		"""Send Sticker to a User/Group.

		Args:
			stickerType (int | str): Sticker type to send
			stickerId (int | str): Sticker id to send
			cateId (int | str): Sticker category id to send
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` objects
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"stickerId": int(stickerId),
				"cateId": int(cateId),
				"type": int(stickerType),
				"clientId": _util.now() + 3000,
				"imei": self._imei,
				"ttl": ttl
			}
		}
		if reply:
			payload["params"]["refMessage"] = str(reply)
		if thread_type == ThreadType.USER:
			url = "https://tt-chat2-wpa.chat.zalo.me/api/message/sticker"
			payload["params"]["zsource"] = 106
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/sticker"
			payload["params"]["zsource"] = 103
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendCustomSticker(self, staticImgUrl, animationImgUrl, thread_id, thread_type, ttl=0, width=0, height=0, pStickerType=0):
		"""Send custom (static/animation) sticker to a User/Group with url.

		Args:
			staticImgUrl (str): Image url (png, jpg, jpeg) format to create sticker
			animationImgUrl (str): Static/Animation image url (webp) format to create sticker
			width (int): Adjust the width of your sticker if it is a gif then it should be something other than 0
			height (int): Adjust the height of your sticker if it is a gif then it should be something other than 0
			thread_id (int | str): User/Group ID to send sticker to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` sticker data
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"clientId": _util.now() + 3000,
				"title": "sticker đã được gửi",
				"oriUrl": staticImgUrl,
				"thumbUrl": staticImgUrl,
				"hdUrl": staticImgUrl,
				"width": width,
				"height": height,
				"properties": json.dumps({
					"subType": 0,
					"color": -1,
					"size": -1,
					"type": 3,
					"ext": json.dumps({
						"sSrcStr": "@STICKER",
						"sSrcType": 0
					})
				}),
				"contentId": _util.now(),
				"thumb_height": height,
				"thumb_width": width,
				"webp": json.dumps({
					"width": width,
					"height": height,
					"url": animationImgUrl
				}),
				"zsource": -1,
				"jcp": json.dumps({
					"pStickerType": pStickerType
				}),
				"ttl": ttl
			}
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/photo_url"
			payload["params"]["toId"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/photo_url"
			payload["params"]["visibility"] = 0
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendImage(self, imagelink, thread_id, thread_type, width=2560, height=2560, message=None, ttl=0):
		"""Send imgae to a User/Group with url.

			Args:
				imagelink (str): Link image to send
				thread_id (int | str): User/Group ID to send to.
				thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP
				width (int): Image width to send
				height (int): Image height to send
				message (Message): Message to send with image

		Returns:
			object: `User/Group` objects response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}
		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/forward"
			payload = {
				"params": {
					'toId': str(thread_id), 
					'imei': self._imei, 
					'ttl': ttl, 
					'zsource': 704, 
					'msgType': 2, 
					'clientId': str(_util.now()), 
					'msgInfo': json.dumps({
						"title": message.text if message else "" or "", 
						"oriUrl": imagelink, 
						"thumbUrl": imagelink, 
						"hdUrl": imagelink, 
						"width": width, 
						"height": height, 
						"properties": json.dumps({
							"color": -1, 
							"size": -1,
							"type": -1, 
							"subType": 0, 
							"ext": json.dumps({
								"sSrcType": -1, 
								"sSrcStr": "", 
								"msg_warning_type": 0, 
								"emoji": {
									"content": 0, 
									"num": 0,
									"uniq": 0, 
									"first": "", 
									"last": "", 
									"most": "", 
									"text": 1
								}
							})
						}), 
						"url": imagelink, 
						"normalUrl": "",
						"jcp": json.dumps({
							"convertible": "jxl"
						})
					})
				}
			}
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/forward"
			payload = {
				"params": {
					'visibility': 0, 
					'ttl': ttl, 
					'zsource': 704, 
					'msgType': 2, 
					'clientId': str(_util.now()), 
					'msgInfo': json.dumps({
						"title": message.text if message else "" or "",
						"oriUrl": str(imagelink),
						"thumbUrl": str(imagelink),
						"hdUrl": str(imagelink),
						"width": width,
						"height": height,
						"properties": None,
						"normalUrl": "",
						"jcp": json.dumps({
							"convertible": "jxl"
						})
					})
				}
			}
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def get_image_size_from_url(self, url):
		headers = {'Range': 'bytes=0-2047'}
		response = requests.get(url, headers=headers)
		response.raise_for_status()
		img_data = BytesIO(response.content)
		img = Image.open(img_data)
		return img.width, img.height

	def sendMultiImage(self, imagelinks, thread_id, thread_type, width=5999, height=9999, message=None, ttl=0):
		"""Send Multiple Image to a User/Group with local file.

		Args:
			imagePathList (list): List image directory to send
			width (int): Image width to send
			height (int): Image height to send
			message (Message): Message to send with image
			thread_id (int | str): User/Group ID to send to.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` objects
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		# uploadData = []
		if ttl == 0:
			ttl = self.ttl
		
		dataUpload = []
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}
		if not isinstance(imagelinks, list) or len(imagelinks) < 1:
			raise ZaloUserError("image path must be a list to be able to send multiple at once.")

		groupLayoutId = _util.now()

		for i, imagelink in enumerate(imagelinks):
			# print(i)
			clientId = str(_util.now() + 100)
			payload = {
				"params": {
					'visibility': 0, 
					'extMsgProp': json.dumps({
						"groupMediaMsg": {
							"groupLayoutId": groupLayoutId
						}
					}), 
					'ttl': ttl, 
					'zsource': 704, 
					'msgType': 2, 
					'clientId': clientId, 
					'msgInfo': json.dumps({
						"title": message.text if message else "" or "", 
						"oriUrl": imagelink, 
						"thumbUrl": imagelink, 
						"hdUrl": imagelink, 
						"width": width, 
						"height": height, 
						"properties": json.dumps({
							"color": -1, 
							"size": -1, 
							"type": -1, 
							"subType": 0, 
							"ext": json.dumps({
								"sSrcType": -1, 
								"sSrcStr": "", 
								"msg_warning_type": 0, 
								"emoji": {
									"content": 0, 
									"num": 0, 
									"uniq": 0, 
									"first": "", 
									"last": "", 
									"most": "", 
									"text": 1
								}
							})
						}),
						"isGroupLayout": 1, 
						"groupLayoutId": groupLayoutId, 
						"totalItemInGroup": len(imagelinks), 
						"idInGroup": i, 
						"clientId": clientId, 
						"url": imagelink, 
						"normalUrl": "", 
						"jcp": json.dumps({
							"convertible": "jxl"
						})
					})
				}
			}
			# print(payload)
			# if i != 0:
				# payload["params"]["msgInfo"]["
			if message and message.mention:
				payload["params"]["mentionInfo"] = message.mention

			if thread_type == ThreadType.USER:
				url = "https://tt-files-wpa.chat.zalo.me/api/message/forward"
				payload["params"]["toid"] = str(thread_id)
			elif thread_type == ThreadType.GROUP:
				url = "https://tt-files-wpa.chat.zalo.me/api/group/forward"
				payload["params"]["grid"] = str(thread_id)
			else:
				raise ZaloUserError("Thread type is invalid")

			payload["params"] = self._encode(payload["params"])
			response = self._post(url, params=params, data=payload)
			data = response.json()
			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("data") else results
				if results == None:
					results = {"error_code": 1337, "error_message": "Data is None"}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						results = {"error_code": 1337, "error_message": results}
			# print(results)
			dataUpload.append(results)

		return dataUpload


	def sendLink(self, linkUrl, title, thread_id, thread_type, thumbnailUrl=None, domainUrl=None, desc=None, message=None, ttl=0):
		"""Send link to a User/Group with url.

		Args:
			linkUrl (str): Link url to send
			domainUrl (str): Main domain of Link to send (eg: github.com)
			thread_id (int | str): User/Group ID to send link to
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP
			thumbnailUrl (str): Thumbnail link url for card to send
			title (str): Title for card to send
			desc (str): Description for card to send
			message (Message): Message object to send with the link

		Returns:
			object: `User/Group` message id response
			dict: A dictionary containing error responses

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"msg": message.text if message else "" or "",
				"href": linkUrl,
				"src": domainUrl or "",
				"title": str(title),
				"desc": desc or "",
				"thumb": thumbnailUrl or "",
				"type": 12,
				"media": json.dumps({
					"type": 12,
					"linkType": 12,
					"count": 0,
					"mediaTitle": "",
					"artist": "",
					"streamUrl": "",
					"stream_icon": ""
				}),
				"ttl": ttl,
				"clientId": _util.now() + 3000
			}
		}

		if message and message.mention:
			payload["params"]["mentionInfo"] = message.mention

		if thread_type == ThreadType.USER:
			url = "https://tt-chat4-wpa.chat.zalo.me/api/message/link"
			payload["params"]["toid"] = str(thread_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/sendlink"
			payload["params"]["imei"] = self._imei
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		# print(payload["params"])
		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendReport(self, thread_id, thread_type, reason=0, content=None):
		"""Send report to Zalo.

		Args:
			reason (int): Reason for reporting

				1 = Nội dung nhạy cảm
				2 = Làm phiền
				3 = Lừa đảo
				0 = custom

			content (str): Report content (work if reason = custom)
			thread_id (int | str): User/Group ID to report
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` send report response

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"uidTo": str(thread_id)
			}
		}

		payload["params"]["content"] = content if content and not reason else None if not content and not reason else ""
		payload["params"]["reason"] = random.randint(1, 3) if content == None else reason

		if thread_type == ThreadType.USER:
			payload["params"]["objId"] = "person.profile"
			payload["params"] = self._encode(payload["params"])
			response = self._post("https://tt-profile-wpa.chat.zalo.me/api/report/abuse-v2", params=params, data=payload)
			data = response.json()
			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("data") else results
				if results == None:
					results = {"error_code": 1337, "error_message": "Data is None"}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						results = {"error_code": 1337, "error_message": results}

				return (
					Group.fromDict(results, None) 
					if thread_type == ThreadType.GROUP else 
					User.fromDict(results, None)
				)

			error_code = data.get("error_code")
			error_message = data.get("error_message") or data.get("data")
			raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		elif thread_type == ThreadType.GROUP:
			payload["params"]["type"] = 14
			payload["params"] = self._encode(payload["params"])
			response = self._post("https://tt-profile-wpa.chat.zalo.me/api/social/profile/reportabuse", params=params, data=payload)
			data = response.json()
			results = data.get("data") if data.get("error_code") == 0 else None
			if results:
				results = self._decode(results)
				results = results.get("data") if results.get("data") else results
				if results == None:
					results = {"error_code": 1337, "error_message": "Data is None"}

				if isinstance(results, str):
					try:
						results = json.loads(results)
					except:
						results = {"error_code": 1337, "error_message": results}

				return (
					Group.fromDict(results, None) 
					if thread_type == ThreadType.GROUP else 
					User.fromDict(results, None)
				)

			error_code = data.get("error_code")
			error_message = data.get("error_message") or data.get("data")
			raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")
		else:
			raise ZaloUserError("Thread type is invalid")




	def qrCode(self, userId):
		"""
		idk
		"""

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}
		payload = {
			"params": {
				'fids': [str(userId)]
			}
		}
		url = "https://tt-friend-wpa.chat.zalo.me/api/friend/mget-qr"
		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			return results

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def hiddenConvers(self, thread_id, thread_type):
		"""Hidden chat

		Args:
			thread_id (int | str): User/Group ID to hidden convers
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` send business card response

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}
		url = "https://tt-convers-wpa.chat.zalo.me/api/hiddenconvers/add-remove"
		if thread_type == ThreadType.USER:
			payload = {
				"params": {
					'del_threads': '[]', 
					'add_threads': json.dumps([{
						"thread_id": thread_id,
						"is_group": 0
					}]), 
					'imei': self._imei
				}
			}
		else:
			payload = {
				"params": {
					'del_threads': '[]', 
					'add_threads': json.dumps([{
						"thread_id": thread_id,
						"is_group": 1
					}]), 
					'imei': self._imei
				}
			}

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def leave(self, thread_id, thread_type, silent=1):
		"""leave group

		Args:
			TUI BỊ LƯỜI ;_;
		Returns:
			object: `User/Group` send business card response

		Raises:
			ZaloAPIException: If request failed
		"""
		if isinstance(thread_id, list):
			GroupId = [str(userid) for userid in thread_id]
		else:
			GroupId = [str(thread_id)]

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}
		url = "https://tt-group-wpa.chat.zalo.me/api/group/leave"
		if thread_type == ThreadType.GROUP:
			payload = {
				"params": {
					'grids': GroupId, 
					'imei': self._imei, 
					'silent': silent, 
					'language': 'vi'
				}
			}

		payload["params"] = self._encode(payload["params"])
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def muteChat(self, thread_id, thread_type, duration=-1, muteType=2, action=1):
		"""Mute Chat

		Args:
			duration (int | str): Time to MuteChat ( -1 to infinity )
			muteType (int | str): Idk default 2
			thread_id (int | str): User/Group ID to hidden convers
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` send business card response

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}
		url = "https://tt-profile-wpa.chat.zalo.me/api/social/profile/setmute"
		if thread_type == ThreadType.GROUP:
			payload = {
				"params": {
					'toid': str(thread_id), 
					'duration': duration, 
					'action': action, 
					'startTime': str(_util.now())[10:], 
					'muteType': muteType, 
					'imei': self._imei
				}
			}

		payload["params"] = self._encode(payload["params"])
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def sendBusinessCard(self, userId, qrCodeUrl, thread_id, thread_type, phone=None, ttl=0):
		"""Send business card by user ID.

		Args:
			userId (int | str): Business card user ID
			qrCodeUrl (str): QR Code link with business card profile information
			phone (int | str): Send business card with phone number
			thread_id (int | str): User/Group ID to change status in
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` send business card response

		Raises:
			ZaloAPIException: If request failed
		"""
		if ttl == 0:
			ttl = self.ttl
		
		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"ttl": ttl,
				"msgType": 6,
				"clientId": str(_util.now() + 3000),
				"msgInfo": {
					"contactUid": str(userId),
					"qrCodeUrl": str(qrCodeUrl)
				}
			}
		}

		if phone:
			payload["params"]["msgInfo"]["phone"] = str(phone)

		if thread_type == ThreadType.USER:
			url = "https://tt-files-wpa.chat.zalo.me/api/message/forward"
			payload["params"]["toId"] = str(thread_id)
			payload["params"]["imei"] = self._imei
		else:
			url = "https://tt-files-wpa.chat.zalo.me/api/group/forward"
			payload["params"]["visibility"] = 0
			payload["params"]["grid"] = str(thread_id)

		payload["params"]["msgInfo"] = json.dumps(payload["params"]["msgInfo"])
		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def SendTransferCard(self, thread_id, thread_type, numAccBank, nameAccBank='---', binBank=970423):
		"""Mute Chat

		Args:
			duration (int | str): Time to MuteChat ( -1 to infinity )
			muteType (int | str): Idk default 2
			thread_id (int | str): User/Group ID to hidden convers
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			object: `User/Group` send business card response

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}
		url = "https://zimsg.chat.zalo.me/api/transfer/card"
		if thread_type == ThreadType.GROUP:
			payload = {
				"params": {
					'binBank': binBank, 
					'numAccBank': numAccBank, 
					'nameAccBank': nameAccBank, 
					'cliMsgId': '1746798881956', 
					'tsMsg': '1746798883254', 
					'destUid': str(thread_id), 
					# 'ttl': 10000, đéo work
					'destType': 1
				}
			}

		payload["params"] = self._encode(payload["params"])
		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			results = results.get("data") if results.get("data") else results
			if results == None:
				results = {"error_code": 1337, "error_message": "Data is None"}

			if isinstance(results, str):
				try:
					results = json.loads(results)
				except:
					results = {"error_code": 1337, "error_message": results}

			return (
				Group.fromDict(results, None) 
				if thread_type == ThreadType.GROUP else 
				User.fromDict(results, None)
			)

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")


	"""
	END SEND METHODS
	"""

	def setTyping(self, thread_id, thread_type):
		"""Set users typing status.

		Args:
			thread_id: User/Group ID to change status in.
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Raises:
			ZaloAPIException: If request failed
		"""
		params = {
			"zpw_ver": 655,
			"zpw_type": 5
		}

		payload = {
			"params": {
				"imei": self._imei
			}
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-chat1-wpa.chat.zalo.me/api/message/typing"
			payload["params"]["toid"] = str(thread_id)
			payload["params"]["destType"] = 5
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/typing"
			payload["params"]["grid"] = str(thread_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			results = self._decode(results)
			return True

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def markAsDelivered(
		self,
		msgId,
		cliMsgId,
		senderId,
		thread_id,
		thread_type,
		seen=0,
		method="webchat"
	):
		"""Mark a message as delivered.

		Args:
			cliMsgId (int | str): Client message ID
			msgId (int | str): Message ID to set as delivered
			senderId (int | str): Message sender Id
			thread_id (int | str): User/Group ID to mark as delivered
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			bool: True

		Raises:
			ZaloAPIException: If request failed
		"""
		destination_id = "0" if thread_type == ThreadType.USER else thread_id

		params = {
			"zpw_ver": 655,
			"zpw_type": 24
		}

		payload = {
			"params": {
				"msgInfos": {
					"seen": seen,
					"data": [{
						"cmi": str(cliMsgId),
						"gmi": str(msgId),
						"si": str(senderId),
						"di": str(destination_id),
						"mt": method,
						"st": 3,
						"at": 0,
						"ts": str(_util.now() + 3000)
					}]
				}
			}
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-chat3-wpa.chat.zalo.me/api/message/deliveredv2"
			payload["params"]["msgInfos"]["data"][0]["cmd"] = 501
		else:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/deliveredv2"
			payload["params"]["msgInfos"]["data"][0]["cmd"] = 521
			payload["params"]["msgInfos"]["grid"] = str(destination_id)
			payload["params"]["imei"] = self._imei

		payload["params"]["msgInfos"] = json.dumps(payload["params"]["msgInfos"])
		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			# self.onMessageDelivered(msgId, thread_id, thread_type, _util.now())
			return True

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	def markAsRead(
		self,
		msgId,
		cliMsgId,
		senderId,
		thread_id,
		thread_type,
		method="webchat"
	):
		"""Mark a message as read.

		Args:
			cliMsgId (int | str): Client message ID
			msgId (int | str): Message ID to set as delivered
			senderId (int | str): Message sender Id
			thread_id (int | str): User/Group ID to mark as read
			thread_type (ThreadType): ThreadType.USER, ThreadType.GROUP

		Returns:
			bool: True

		Raises:
			ZaloAPIException: If request failed
		"""
		destination_id = "0" if thread_type == ThreadType.USER else thread_id

		params = {
			"zpw_ver": 655,
			"zpw_type": 24,
			"nretry": 0
		}

		payload = {
			"params": {
				"msgInfos": {
					"data": [{
						"cmi": str(cliMsgId),
						"gmi": str(msgId),
						"si": str(senderId),
						"di": str(destination_id),
						"mt": method,
						"st": 3,
						"ts": str(_util.now())
					}]
				},
				"imei": self._imei
			}
		}

		if thread_type == ThreadType.USER:
			url = "https://tt-chat1-wpa.chat.zalo.me/api/message/seenv2"
			payload["params"]["msgInfos"]["data"][0]["at"] = 7
			payload["params"]["msgInfos"]["data"][0]["cmd"] = 501
			payload["params"]["senderId"] = str(destination_id)
		elif thread_type == ThreadType.GROUP:
			url = "https://tt-group-wpa.chat.zalo.me/api/group/seenv2"
			payload["params"]["msgInfos"]["data"][0]["at"] = 0
			payload["params"]["msgInfos"]["data"][0]["cmd"] = 511
			payload["params"]["grid"] = str(destination_id)
		else:
			raise ZaloUserError("Thread type is invalid")

		payload["params"]["msgInfos"] = json.dumps(payload["params"]["msgInfos"])
		payload["params"] = self._encode(payload["params"])

		response = self._post(url, params=params, data=payload)
		data = response.json()
		results = data.get("data") if data.get("error_code") == 0 else None
		if results:
			# self.onMarkedSeen(msgId, thread_id, thread_type, _util.now())
			return True

		error_code = data.get("error_code")
		error_message = data.get("error_message") or data.get("data")
		raise ZaloAPIException(f"Error #{error_code} when sending requests: {error_message}")

	"""
	LISTEN METHODS
	"""

	def _listen_req(self, delay=1, thread=False, reconnect=5):
		self._condition.clear()
		HasRead = set()

		try:
			self.onListening()
			self._listening = True

			while not self._condition.is_set():
				ListenTime = int((time.time() - 10) * 1000)

				if len(HasRead) > 10000000:
					HasRead.clear()

				messages = self.getLastMsgs()
				groupmsg = messages.groupMsgs
				messages = messages.msgs

				for message in messages + groupmsg:
					if int(message["ts"]) >= ListenTime and message["msgId"] not in HasRead:
						HasRead.add(message["msgId"])
						msgObj = MessageObject.fromDict(message, None)
						if (msgObj.msgType == "chat.photo" and ("tracking" in msgObj.content.get('params', '') or "webp" in msgObj.content.get('params', ''))):
							msgObj.msgType = "custom.sticker"

						if message in messages:

							[
								pool.submit(self.onMessage, msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.uidFrom) or msgObj.idTo), ThreadType.USER)
								if thread else
								self.onMessage(msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.uidFrom) or msgObj.idTo), ThreadType.USER)
							]

						else:

							[
								pool.submit(self.onMessage, msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.idTo) or self.uid), ThreadType.GROUP)
								if thread else
								self.onMessage(msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.idTo) or self.uid), ThreadType.GROUP)
							]

				time.sleep(delay)

		except KeyboardInterrupt:
			self._condition.set()
			print("\x1b[1K")
			logger.warning("Stop Listen Because KeyboardInterrupt Exception!")
			pid = os.getpid()
			os.kill(pid, signal.SIGTERM)

		except Exception as e:
			self._condition.set()
			self._listening = False
			self.onErrorCallBack(e)
			if self.run_forever:
				while not self._listening:
					try:
						logger.debug("Run forever mode is enabled, trying to reconnect...")
						self._listen_req(delay, thread, reconnect)
					except:
						pass

					time.sleep(reconnect)
			pid = os.getpid()
			os.kill(pid, signal.SIGTERM)
		finally:
			self._listening = False
			pid = os.getpid()
			os.kill(pid, signal.SIGTERM)
			
	def _listen_ws(self, thread=False, reconnect=5, type=0):
		self._condition.clear()
		if type == 0:
			params = {"zpw_ver": 655, "zpw_type": 24, "t": _util.now()}
		else:
			params = {"zpw_ver": 655, "zpw_type": 30, "t": _util.now()}
		url = self._state._config["zpw_ws"][random.randint(0, 4)] + "?" + urllib.parse.urlencode(params)
		user_agent = self._state._headers.get("User-Agent") or _util.HEADERS["User-Agent"]
		raw_cookies = _util.dict_to_raw_cookies(self._state.get_cookies())
		
		if not raw_cookies:
			raise ZaloUserError("Unable to load cookies! Probably due to incorrect cookie format (cookies must be dict)")
		
		headers = {
			"Accept-Encoding": "gzip, deflate, br, zstd",
			"Accept-Language": "en-US,en;q=0.9",
			"Cache-Control": "no-cache",
			"Connection": "Upgrade",
			"Host": urlparse(url).netloc,
			"Origin": "https://chat.zalo.me",
			"Pragma": "no-cache",
			"Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits",
			"Sec-WebSocket-Version": "13",
			"Upgrade": "websocket",
			"User-Agent": user_agent,
			"Cookie": raw_cookies,
		}
		
		def on_open(ws):
			self.listening = True
			self.onListening()
			
			
		def on_close(ws, status_code, msg):
			ws.close()
			if self.listening:
				self._listen_ws(type=type)
				
			
			
			
		def on_error(ws, error):
			if isinstance(error, KeyboardInterrupt):
				ws.close()
				print('')
				logger.warning("Stop Listen Because KeyboardInterrupt Exception!")
				pid = os.getpid()
				os.kill(pid, signal.SIGTERM)
				self.onErrorCallBack(error)
			else:
				ws.close()
				if self.listening:
					self._listen_ws(type=type)
				
		def on_ping(ws, message):
			pass
			#print(f"Ping received: {message}")
			
		def on_pong(ws, message):
			# self.ws_ping_scheduler
			pass
			#print(f"Pong received: {message}")
			
		def on_cont_message(ws, message, is_last):
			pass
			
		def on_data(ws, data, data_type, continue_flag):
			#print(f"Data: {data}, Type: {data_type}, Continue: {continue_flag}")
			if continue_flag == True:
				pass
			else:
				ws.close()
				if self.listening:
					self._listen_ws(type=type)
			
		def on_message(ws, data):
			if not isinstance(data, bytes):
				return
			
			try:
				encodedHeader = data[:4]
				version, cmd, subCmd = _util.getHeader(encodedHeader)
				dataToDecode = data[4:]
				decodedData = dataToDecode.decode("utf-8")
				# print(decodedData)
				if not decodedData or "eventId" in decodedData:
					return
				
				if not isinstance(decodedData, dict):
					parsed = json.loads(decodedData)
				
				parsed = json.loads(decodedData)
				# if self._imei == "Tahn":
					# print(f'ver: {version}, cmd: {cmd}, sub: {subCmd} data: {parsed}')
				if version == 1 and cmd == 1 and subCmd == 1:
					# print(parsed)
					# print(type(parsed))
					if "key" in parsed:
						self.ws_key = parsed["key"]
						self.ws_ping_scheduler()
						# self.ws.send(b'\x014\x02\x00{"data":{"platformType":1},"reqId":"signal_req_0"}', websocket.ABNF.OPCODE_BINARY)
						return
					elif "error_code" in parsed and "error_message" in parsed and parsed.get("error_code") == 600 and parsed.get("error_message") == "zpw_sek bị thiếu hoặc không đúng":
						# print("if done, no big")
						logger.warning("Stop Listen Because Logout Session By User")
						pool.submit(self.eventSignOutSession)
						ws.close()
						# self.stopListening()
						self.listening = False
						return
				if not hasattr(self, "ws_key"):
					return logger.error("Unable to decrypt data because key not found")

				parsedData = _util.zws_decode(parsed, self.ws_key)
				# if self._imei == "Tahn":
					# print(f"v: {version}, cmd: {cmd}, s: {subCmd} data: {parsedData}")
				if version == 1 and cmd == 3000 and subCmd == 0:
					logger.warning(f"Another connection is opened, closing this one from imei {self._imei}")
					ws.close()
					if type == 0:
						self._listen_ws(type=1)
					elif type == 1:
						self._listen_ws(type=0)

				elif version == 1 and (cmd == 511 or cmd == 510) and subCmd == 1:
					pool.submit(self.eventGetOldMessage, MessageObject.fromDict(parsedData["data"], None))
				elif version == 1 and cmd == 501 and subCmd == 0:
					# v: 1, cmd: 501, s: 0 data: {'error_code': 0, 'error_message': '', 'data': {'lastActionId': '11827119155954', 'more': 0, 'msgs': [], 'groupMsgs': [], 'pageMsgs': [{'actionId': '11827119155954', 'msgId': '7272069020162', 'cliMsgId': '1764326358072', 'msgType': 'webchat', 'uidFrom': '0', 'idTo': '2552105658566092457', 'dName': '-tahnᅐᅐᅡ', 'ts': '1764326358156', 'status': 1, 'content': '.', 'notify': '1', 'ttl': 0, 'userId': '0', 'uin': '0', 'topOut': '0', 'topOutTimeOut': '0', 'topOutImprTimeOut': '0', 'quote': {'ownerId': 2552105658566092457, 'cliMsgId': 1764326271940, 'globalMsgId': 7272064223159, 'cliMsgType': 1, 'ts': 1764326271941, 'msg': 'Hệ thống đã hủy Yêu cầu xóa tài khoản Zalo của số điện thoại +84357293117.', 'attach': '', 'fromD': 'Zalo', 'ttl': 2592000000}, 'propertyExt': {'color': -1, 'size': -1, 'type': -1, 'subType': 0, 'ext': '{"sSrcType":-1,"sSrcStr":"","msg_warning_type":0,"emoji":{"content":0,"num":0,"uniq":0,"first":"","last":"","most":"","text":1}}'}, 'paramsExt': {'countUnread': 1, 'containType': 0, 'platformType': 0}, 'cmd': 501, 'st': 3, 'at': 0, 'realMsgId': '0'}], 'clearUnreads': [], 'delivereds': [], 'seens': [], 'groupSeens': [], 'queueStatus': {'510_0': {'ids': ['11827119155954'], 'lastId': '11827119155954'}, '510_1': {'ids': ['7272069020162'], 'lastId': '7272069020162'}}, 'eesession': []}}
					# v: 1, cmd: 501, s: 0 data: {'error_code': 0, 'error_message': '', 'data': {'lastActionId': '11827173911818', 'more': 0, 'msgs': [], 'groupMsgs': [], 'pageMsgs': [{'actionId': '11827173911818', 'msgId': '7272094776350', 'cliMsgId': '1764326819709', 'msgType': 'webchat', 'uidFrom': '2552105658566092457', 'idTo': '0', 'dName': 'Zalo', 'ts': '1764326819709', 'status': 1, 'content': 'Hệ thống đã hủy Yêu cầu xóa tài khoản Zalo của số điện thoại +84357293117.', 'notify': '1', 'ttl': 2592000000, 'userId': '0', 'uin': '0', 'topOut': '1', 'topOutTimeOut': '604800', 'topOutImprTimeOut': '604800', 'paramsExt': {'countUnread': 0, 'containType': 0, 'platformType': 3}, 'cmd': 501, 'st': 5, 'at': 0, 'realMsgId': '0'}], 'clearUnreads': [], 'delivereds': [], 'seens': [], 'groupSeens': [], 'queueStatus': {'510_0': {'ids': ['11827173911818'], 'lastId': '11827173911818'}, '510_1': {'ids': ['7272094776350'], 'lastId': '7272094776350'}}, 'eesession': []}}
# v: 1, cmd: 601, s: 0 data: {'error_code': 0, 'error_message': '', 'data': {'lastActionId': '1395148830711', 'more': 0, 'controls': [{'actionId': '1395148830711', 'controlId': '1033573864843', 'content': {'act': 'update', 'data': '{}', 'act_type': 'profile'}}], 'reacts': [], 'reactGroups': [], 'queueStatus': {'603_1': {'ids': ['1033573864843'], 'lastId': '1033573864843'}, '604_0': {'ids': [], 'lastId': '0'}, '603_0': {'ids': ['1395148830711'], 'lastId': '1395148830711'}}}}
					if parsedData["data"]["msgs"]:
						userMsgs = parsedData["data"]["msgs"]
						message = userMsgs[0]
					elif parsedData["data"]["pageMsgs"]:
						pageMsgs = parsedData["data"]["pageMsgs"]
						message = pageMsgs[0]
					# print(message)
					msgObj = MessageObject.fromDict(message, None)
					[
						pool.submit(self.onMessage, msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.uidFrom) or msgObj.idTo), ThreadType.USER)
						if self.thread else
						self.onMessage(msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.uidFrom) or msgObj.idTo), ThreadType.USER)
					]

				elif version == 1 and cmd == 521 and subCmd == 0:
					groupMsgs = parsedData["data"]["groupMsgs"]
					if groupMsgs == []:
						print(parsedData)
						return
					message = groupMsgs[0]

					msgObj = MessageObject.fromDict(message, None)
					if (msgObj.msgType == "chat.photo" and ("tracking" in msgObj.content.get('params', '') or "webp" in msgObj.content.get('params', ''))):
						msgObj.msgType = "custom.sticker"
					[
						pool.submit(self.onMessage, msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.idTo) or self.uid), ThreadType.GROUP)
						if self.thread else
						self.onMessage(msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.idTo) or self.uid), ThreadType.GROUP)
					]
				elif version == 1 and cmd == 522 and subCmd == 0:
					# print(parsedData)
					data = parsedData["data"]
					# print(data)
					# {'msgId': '7248510872387', 'seen': 0, 'deliveredUids': ['3194032149880148112'], 'seenUids': [], 'realMsgId': '0', 'groupId': '8579119843336951756', 'mSTs': -1}
					if data["delivereds"]:
						for i in data["delivereds"]:
							pool.submit(self.onMessageDelivered, i["msgId"], i["deliveredUids"], i["groupId"], ThreadType.GROUP, _util.now(), EventObject.fromDict(parsedData))
					elif data["groupSeens"]:
						for i in data["groupSeens"]:
							pool.submit(self.onMarkedSeen, i["msgId"], i["seenUids"], i["groupId"], ThreadType.GROUP, _util.now(), EventObject.fromDict(parsedData))
					# onMessageDelivered
					# v: 1, cmd: 522, s: 0 data: {'error_code': 0, 'error_message': '', 'data': {'more': 0, 'msgs': [], 'groupMsgs': [], 'pageMsgs': [], 'clearUnreads': [], 'delivereds': [], 'seens': [], 'groupSeens': [{'msgId': '7248513413462', 'groupId': '8579119843336951756', 'seenUids': ['8671295734933406101']}], 'eesession': []}}
				elif version == 1 and cmd == 601 and subCmd == 0:
					controls = parsedData["data"].get("controls", [])
					# if 'groupMsgs' in parsedData['data'] and parsedData['data']['groupMsgs']:
						# dat = parsedData["data"].get("groupMsgs", [])
						# pool.submit(self.onEvent, event_data=dat, event_type='yeucauthamgia')
					# print(controls)
					for control in controls:
						# print(control)
						if control["content"]["act_type"] == "group":

							if control["content"]["act"] == "join_request":
								groupEventType = control["content"]["act"]
								# print(control["content"]["data"]["groupId"])
								# print(self.viewGroupPending(control["content"]["data"]["groupId"]))
								data = self.viewGroupPending(control["content"]["data"]["groupId"])
								data["groupId"] = control["content"]["data"]["groupId"]
								event_data = EventObject.fromDict(data)
								event_type = groupEventType
								[
									pool.submit(self.onEvent, event_data, event_type)
									if thread else
									self.onEvent(event_data, event_type)
								]
								return 

							groupEventData = json.loads(control["content"]["data"]) if isinstance(control["content"]["data"], str) else control["content"]["data"]
							groupEventType = _util.getGroupEventType(control["content"]["act"]) if _util.getGroupEventType(control["content"]["act"]) != GroupEventType.UNKNOWN else control["content"]["act"]
							event_data = EventObject.fromDict(groupEventData)
							event_type = groupEventType
							[
								pool.submit(self.onEvent, event_data, event_type)
								if thread else
								self.onEvent(event_data, event_type)
							]

						elif control["content"]["act_type"] == "fr":
							pool.submit(self.friendEvent, data=EventObject.fromDict(control), type=control["content"]["act"])

						elif control["content"]["act_type"] == "voip":
							pool.submit(self.callEvent, data=control)

						elif control["content"]["act_type"] == 'file':
							pool.submit(self.renewEvent, data=control, type=control["content"]["act"])




				elif version == 1 and cmd == 602 and subCmd == 0:
					action = parsedData["data"]["actions"]
					for i in action:
						act_type = i["act_type"]
						act = i["act"]
						if act_type == "typing":
							if act == "gtyping":
								data = EventObject.fromDict(json.loads("{" + i["data"] + "}"))
								pool.submit(self.typingEvent, data, ThreadType.GROUP)
							elif act == "typing":
								data = EventObject.fromDict(json.loads("{" + i["data"] + "}"))
								pool.submit(self.typingEvent, data, ThreadType.USER)
							
					
				elif cmd == 612:
					reacts = parsedData["data"].get("reacts", [])
					reactGroups = parsedData["data"].get("reactGroups", [])

					for react in reacts:
						react["content"] = json.loads(react["content"])
						msgObj = MessageObject.fromDict(react, None)
						[
							pool.submit(self.onMessage, msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.uidFrom) or msgObj.idTo), ThreadType.USER)
							if self.thread else
							self.onMessage(msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.uidFrom) or msgObj.idTo), ThreadType.USER)
						]

					for reactGroup in reactGroups:
						reactGroup["content"] = json.loads(reactGroup["content"])
						msgObj = MessageObject.fromDict(reactGroup, None)
						[
							pool.submit(self.onMessage, msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.idTo) or self.uid), ThreadType.GROUP)
							if self.thread else
							self.onMessage(msgObj.msgId, str(int(msgObj.uidFrom) or self.uid), msgObj.content, msgObj, str(int(msgObj.idTo) or self.uid), ThreadType.GROUP)
						]

			except Exception as e:
				self.onErrorCallBack(e)


		ws = websocket.WebSocketApp(
			url,
			header=headers,
			on_message=on_message,
			on_error=on_error,
			on_close=on_close,
			on_open=on_open,
			on_ping=on_ping,
			on_pong=on_pong,
			on_cont_message=on_cont_message,
			on_data=on_data
		)

		self.ws = ws
		self.thread = thread

		ws.run_forever(reconnect=True, sslopt={"cert_reqs": ssl.CERT_NONE, "check_hostname": False})


	def ws_ping_scheduler(self):
		try:
			payload = {
				"version": 1,
				"cmd": 2,
				"subCmd": 1,
				"data": {"eventId": int(time.time() * 1000)}
			}
			encoded_data = json.dumps(payload["data"]).encode()
			data_length = len(encoded_data)
			header = struct.pack("<BIB", payload["version"], payload["cmd"], payload["subCmd"])
			data = header + encoded_data
			self.ws.send(data, websocket.ABNF.OPCODE_BINARY)
			if self.listening == False:
				return
			self.ping_interval = threading.Timer(3 * 60, self.ws_ping_scheduler)
			self.ping_interval.start()
		except websocket.WebSocketConnectionClosedException as e:
			self.onErrorCallBack(e)
			try:
				self.ws.close()
			except websocket.WebSocketConnectionClosedException as e:
				self._listen_ws()
		except Exception as e:
			self.onErrorCallBack(e)
			try:
				self.ws.close()
			except websocket.WebSocketConnectionClosedException as e:
				self._listen_ws()


	def request_old_messages(self, thread_type, last_msg_id, first=True):
		if not self.ws:
			return
		payload = {
			"first": first, 
			"lastId": int(last_msg_id), 
			"preIds": [],
			"req_id": f"req_{self.request_id}"
		}
		self.request_id += 1
		cmd = 510 if thread_type == ThreadType.USER else 511
		encoded_data = json.dumps(payload).encode('utf-8')
		data_length = len(encoded_data)
		buffer = bytearray(4 + data_length)
		buffer[0] = 1
		struct.pack_into('<i', buffer, 1, cmd)
		buffer[3] = 1
		buffer[4:] = encoded_data
		self.ws.send(buffer, websocket.ABNF.OPCODE_BINARY)
		return self.getMsgEvent.get()

	def request_old_reactions(self, thread_type, last_msg_id, first=True):
		if not self.ws:
			return

		payload = {
			"first": first, 
			"lastId": last_msg_id, 
			"preIds": [],
			"req_id": f"req_{self.request_id}"
		}
		self.request_id += 1
		cmd = 510 if thread_type == ThreadType.USER else 511
		encoded_data = json.dumps(payload).encode('utf-8')
		data_length = len(encoded_data)
		buffer = bytearray(4 + data_length)
		buffer[0] = 1
		struct.pack_into('<i', buffer, 1, cmd)
		buffer[3] = 1
		buffer[4:] = encoded_data
		self.ws.send(buffer, websocket.ABNF.OPCODE_BINARY)



	def startListening(self, delay=1, thread=False, type="websocket", reconnect=5):
		"""Start listening from an external event loop.

		Args:
			delay (int): Delay time each time fetching a message
			thread (bool): Handle messages within the thread (Default: False)
			type (str): Type of listening (Default: websocket)
			reconnect (int): Delay interval when reconnecting

		Raises:
			ZaloAPIException: If request failed
		"""
		if str(type).lower() == "websocket":

			if self._state._config.get("zpw_ws"):
				self._listen_ws(thread, reconnect)

			else:
				logger.debug("WebSocket url not found. Listen will switch to `requests` mode")
				self._listen_req(delay, thread)

		elif str(type).lower() == "requests":
			self._listen_req(delay, thread)

		else:
			raise ZaloUserError("Invalid listen type, only `websocket` or `requests`")

	def stopListening(self):
		"""Stop the listening loop."""
		self.listening = False
		self._condition.set()

	def listen(self, delay=1, thread=False, type="websocket", run_forever=False, reconnect=5):
		"""Initialize and runs the listening loop continually.

		Args:
			delay (int): Delay time for each message fetch (Default: 1)
			thread (bool): Handle messages within the thread (Default: False)
			type (str): Type of listening (Default: websocket)
			reconnect (int): Delay interval when reconnecting
		"""
		self.run_forever = run_forever
		self.startListening(delay, thread, type, reconnect)

	"""
	END LISTEN METHODS
	"""

	"""
	EVENTS
	"""

	def onLoggingIn(self, phone=None):
		"""Called when the client is logging in.

		Args:
			phone: The phone number of the client
		"""
		logger.debug("Logging in {}...".format(phone))

	def onLoggedIn(self, phone=None):
		"""Called when the client is successfully logged in.

		Args:
			phone: The phone number of the client
		"""
		logger.login("Login of {} successful.".format(phone))

	def onListening(self):
		"""Called when the client is listening."""
		logger.debug("Listening...")

	def onMessage(self, mid=None, author_id=None, message=None, message_object=None, thread_id=None, thread_type=ThreadType.USER):
		"""Called when the client is listening, and somebody sends a message.

		Args:
			mid: The message ID
			author_id: The ID of the author
			message: The message content of the author
			message_object: The message (As a `Message` object)
			thread_id: Thread ID that the message was sent to.
			thread_type (ThreadType): Type of thread that the message was sent to.
		"""
		
		logger.info("{} from {} in {}".format(message, thread_id, thread_type.name))

	def friendEvent(self, data, type):
		"""send a friend request.
		"""

	def typingEvent(self, data, thread_type):
		"""Typing Event
		"""

		# print(data, thread_type.name)

	def callEvent(self, data):
		"""CallEvent
		"""

	def renewEvent(self, data, type):
		"""File status Event
		"""
		print(data)
		if type == "renew_link":
			self.renew_link = json.loads(data["content"]["data"])
			print(self.renew_link)
			if self.renew_link != None:
				for i in self.renew_link:
					self.fileEvent.put(i)

	def onEvent(self, event_data, event_type):
		"""Called when the client listening, and some events occurred.

		Args:
			event_data (EventObject): Event data (As a `EventObject` object)
			event_type (EventType/GroupEventType): Event Type
		"""

	def onMessageDelivered(self, msg_ids=None, author_id=None, thread_id=None, thread_type=ThreadType.USER, ts=None, data=None):
		"""Called when the client is listening, and the client has successfully marked messages as delivered.

		Args:
			msg_ids: The messages that are marked as delivered
			thread_id: Thread ID that the action was sent to
			thread_type (ThreadType): Type of thread that the action was sent to
			ts: A timestamp of the action
		"""
		# logger.info("Marked messages {} by {} as delivered in [({}, {})] at {}.".format(msg_ids, author_id, thread_id, thread_type.name, int(ts / 1000)))

	def onMarkedSeen(self, msg_ids=None, author_id=None, thread_id=None, thread_type=ThreadType.USER, ts=None, data=None):
		"""Called when the client is listening, and the client has successfully marked messages as read/seen.

		Args:
			msg_ids: The messages that are marked as read/seen
			thread_id: Thread ID that the action was sent to
			thread_type (ThreadType): Type of thread that the action was sent to
			ts: A timestamp of the action
		"""
		# logger.info("Marked messages {} by {} as seen in [({}, {})] at {}.".format(msg_ids, author_id, thread_id, thread_type.name, int(ts / 1000)))
	
	def eventSignOutSession(self):
		"""Called when the client logout session, stop loop try reconnect wss.
		"""
	def eventGetOldMessage(self, data):
		#tao bị ngu
		"""
		tao bị ngu 
		"""
		self.getMsgEvent.put(data)
	

	def onErrorCallBack(self, error, ts=int(time.time())):
		"""Called when the module has some error.

		Args:
			error: Description of the error
			ts: A timestamp of the error (Default: auto)
		"""
		logger.error(f"An error occurred at {ts}: {error}")
		print(traceback.format_exc())

	"""
	END EVENTS
	"""