wasender-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wasender/__init__.py +2 -0
- wasender/client.py +63 -0
- wasender/contacts.py +69 -0
- wasender/groups.py +96 -0
- wasender/main.py +45 -0
- wasender/messages.py +161 -0
- wasender/output.py +31 -0
- wasender/sessions.py +72 -0
- wasender_cli-0.1.0.dist-info/METADATA +75 -0
- wasender_cli-0.1.0.dist-info/RECORD +14 -0
- wasender_cli-0.1.0.dist-info/WHEEL +5 -0
- wasender_cli-0.1.0.dist-info/entry_points.txt +2 -0
- wasender_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- wasender_cli-0.1.0.dist-info/top_level.txt +1 -0
wasender/__init__.py
ADDED
wasender/client.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
from .output import print_http_error_and_exit, print_error_and_exit
|
|
5
|
+
|
|
6
|
+
class WasenderClient:
|
|
7
|
+
def __init__(self, token: str, base_url: str = "https://wasenderapi.com/api"):
|
|
8
|
+
self.token = token
|
|
9
|
+
self.base_url = base_url.rstrip('/')
|
|
10
|
+
self.headers = {
|
|
11
|
+
"Authorization": f"Bearer {self.token}",
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
"Accept": "application/json"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
17
|
+
try:
|
|
18
|
+
response.raise_for_status()
|
|
19
|
+
text = response.text.strip()
|
|
20
|
+
if not text:
|
|
21
|
+
return {}
|
|
22
|
+
try:
|
|
23
|
+
return response.json()
|
|
24
|
+
except ValueError:
|
|
25
|
+
print_error_and_exit(f"Invalid JSON response. Content snippet: {text[:100]}")
|
|
26
|
+
except httpx.HTTPStatusError as e:
|
|
27
|
+
try:
|
|
28
|
+
error_msg = response.json()
|
|
29
|
+
except Exception:
|
|
30
|
+
error_msg = response.text
|
|
31
|
+
print_http_error_and_exit(e.response.status_code, str(error_msg))
|
|
32
|
+
except httpx.RequestError as e:
|
|
33
|
+
print_error_and_exit(f"Connection error: {e}")
|
|
34
|
+
|
|
35
|
+
def request(self, method: str, path: str, **kwargs) -> Any:
|
|
36
|
+
url = f"{self.base_url}/{path.lstrip('/')}"
|
|
37
|
+
|
|
38
|
+
# Don't pass auth down implicitly, we already handle it in headers
|
|
39
|
+
# but just passing headers down
|
|
40
|
+
req_kwargs = {"headers": self.headers}
|
|
41
|
+
req_kwargs.update(kwargs)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
with httpx.Client() as client:
|
|
45
|
+
response = client.request(method, url, **req_kwargs)
|
|
46
|
+
return self._handle_response(response)
|
|
47
|
+
except httpx.RequestError as e:
|
|
48
|
+
print_error_and_exit(f"Connection error: {e}")
|
|
49
|
+
|
|
50
|
+
def get(self, path: str, params: Optional[Dict] = None) -> Any:
|
|
51
|
+
return self.request("GET", path, params=params)
|
|
52
|
+
|
|
53
|
+
def post(self, path: str, json: Optional[Dict] = None) -> Any:
|
|
54
|
+
return self.request("POST", path, json=json)
|
|
55
|
+
|
|
56
|
+
def put(self, path: str, json: Optional[Dict] = None) -> Any:
|
|
57
|
+
return self.request("PUT", path, json=json)
|
|
58
|
+
|
|
59
|
+
def patch(self, path: str, json: Optional[Dict] = None) -> Any:
|
|
60
|
+
return self.request("PATCH", path, json=json)
|
|
61
|
+
|
|
62
|
+
def delete(self, path: str) -> Any:
|
|
63
|
+
return self.request("DELETE", path)
|
wasender/contacts.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from .output import print_json_obj
|
|
3
|
+
from .client import WasenderClient
|
|
4
|
+
|
|
5
|
+
app = typer.Typer()
|
|
6
|
+
|
|
7
|
+
def get_client(ctx: typer.Context) -> WasenderClient:
|
|
8
|
+
return ctx.obj
|
|
9
|
+
|
|
10
|
+
@app.command("list")
|
|
11
|
+
def list_contacts(ctx: typer.Context, session_id: str):
|
|
12
|
+
client = get_client(ctx)
|
|
13
|
+
result = client.get(f"sessions/{session_id}/contacts")
|
|
14
|
+
print_json_obj(result)
|
|
15
|
+
|
|
16
|
+
@app.command("add")
|
|
17
|
+
def add_or_edit_contact(ctx: typer.Context, session_id: str, phone: str, name: str):
|
|
18
|
+
client = get_client(ctx)
|
|
19
|
+
payload = {"phone": phone, "name": name}
|
|
20
|
+
result = client.post(f"sessions/{session_id}/contacts", json=payload)
|
|
21
|
+
print_json_obj(result)
|
|
22
|
+
|
|
23
|
+
@app.command("get")
|
|
24
|
+
def get_contact_info(ctx: typer.Context, session_id: str, jid: str):
|
|
25
|
+
client = get_client(ctx)
|
|
26
|
+
result = client.get(f"sessions/{session_id}/contacts/{jid}")
|
|
27
|
+
print_json_obj(result)
|
|
28
|
+
|
|
29
|
+
@app.command("picture")
|
|
30
|
+
def get_contact_picture(ctx: typer.Context, session_id: str, jid: str):
|
|
31
|
+
client = get_client(ctx)
|
|
32
|
+
result = client.get(f"sessions/{session_id}/contacts/{jid}/picture")
|
|
33
|
+
print_json_obj(result)
|
|
34
|
+
|
|
35
|
+
@app.command("block")
|
|
36
|
+
def block_contact(ctx: typer.Context, session_id: str, jid: str):
|
|
37
|
+
client = get_client(ctx)
|
|
38
|
+
result = client.post(f"sessions/{session_id}/contacts/{jid}/block")
|
|
39
|
+
print_json_obj(result)
|
|
40
|
+
|
|
41
|
+
@app.command("unblock")
|
|
42
|
+
def unblock_contact(ctx: typer.Context, session_id: str, jid: str):
|
|
43
|
+
client = get_client(ctx)
|
|
44
|
+
result = client.post(f"sessions/{session_id}/contacts/{jid}/unblock")
|
|
45
|
+
print_json_obj(result)
|
|
46
|
+
|
|
47
|
+
@app.command("check")
|
|
48
|
+
def check_jid_on_whatsapp(ctx: typer.Context, session_id: str, phone: str):
|
|
49
|
+
client = get_client(ctx)
|
|
50
|
+
result = client.get(f"sessions/{session_id}/contacts/check/{phone}")
|
|
51
|
+
print_json_obj(result)
|
|
52
|
+
|
|
53
|
+
@app.command("lid")
|
|
54
|
+
def get_lid_from_phone_number(ctx: typer.Context, session_id: str, phone: str):
|
|
55
|
+
client = get_client(ctx)
|
|
56
|
+
result = client.get(f"sessions/{session_id}/contacts/lid/{phone}")
|
|
57
|
+
print_json_obj(result)
|
|
58
|
+
|
|
59
|
+
@app.command("phone")
|
|
60
|
+
def get_phone_number_from_lid(ctx: typer.Context, session_id: str, lid: str):
|
|
61
|
+
client = get_client(ctx)
|
|
62
|
+
result = client.get(f"sessions/{session_id}/contacts/phone/{lid}")
|
|
63
|
+
print_json_obj(result)
|
|
64
|
+
|
|
65
|
+
@app.command("search")
|
|
66
|
+
def search_contacts(ctx: typer.Context, session_id: str, query: str):
|
|
67
|
+
client = get_client(ctx)
|
|
68
|
+
result = client.get(f"sessions/{session_id}/contacts/search", params={"q": query})
|
|
69
|
+
print_json_obj(result)
|
wasender/groups.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from .output import print_json_obj
|
|
4
|
+
from .client import WasenderClient
|
|
5
|
+
|
|
6
|
+
app = typer.Typer()
|
|
7
|
+
|
|
8
|
+
def get_client(ctx: typer.Context) -> WasenderClient:
|
|
9
|
+
return ctx.obj
|
|
10
|
+
|
|
11
|
+
@app.command("list")
|
|
12
|
+
def list_groups(ctx: typer.Context, session_id: str):
|
|
13
|
+
client = get_client(ctx)
|
|
14
|
+
result = client.get(f"sessions/{session_id}/groups")
|
|
15
|
+
print_json_obj(result)
|
|
16
|
+
|
|
17
|
+
@app.command("create")
|
|
18
|
+
def create_group(ctx: typer.Context, session_id: str, name: str, participants: str = typer.Option(..., help="Comma separated jids")):
|
|
19
|
+
client = get_client(ctx)
|
|
20
|
+
payload = {"name": name, "participants": participants.split(",")}
|
|
21
|
+
result = client.post(f"sessions/{session_id}/groups", json=payload)
|
|
22
|
+
print_json_obj(result)
|
|
23
|
+
|
|
24
|
+
@app.command("metadata")
|
|
25
|
+
def get_group_metadata(ctx: typer.Context, session_id: str, group_id: str):
|
|
26
|
+
client = get_client(ctx)
|
|
27
|
+
result = client.get(f"sessions/{session_id}/groups/{group_id}")
|
|
28
|
+
print_json_obj(result)
|
|
29
|
+
|
|
30
|
+
@app.command("participants")
|
|
31
|
+
def get_group_participants(ctx: typer.Context, session_id: str, group_id: str):
|
|
32
|
+
client = get_client(ctx)
|
|
33
|
+
result = client.get(f"sessions/{session_id}/groups/{group_id}/participants")
|
|
34
|
+
print_json_obj(result)
|
|
35
|
+
|
|
36
|
+
@app.command("add")
|
|
37
|
+
def add_group_participants(ctx: typer.Context, session_id: str, group_id: str, participants: str = typer.Option(..., help="Comma separated jids")):
|
|
38
|
+
client = get_client(ctx)
|
|
39
|
+
payload = {"participants": participants.split(",")}
|
|
40
|
+
result = client.post(f"sessions/{session_id}/groups/{group_id}/participants", json=payload)
|
|
41
|
+
print_json_obj(result)
|
|
42
|
+
|
|
43
|
+
@app.command("remove")
|
|
44
|
+
def remove_group_participants(ctx: typer.Context, session_id: str, group_id: str, participants: str = typer.Option(..., help="Comma separated jids")):
|
|
45
|
+
client = get_client(ctx)
|
|
46
|
+
# DELETE might not take a body based on std conventions, but if Wasender does, we pass json here.
|
|
47
|
+
payload = {"participants": participants.split(",")}
|
|
48
|
+
# Usually Wasender has a specific POST endpoint for removing or accepts DELETE with body
|
|
49
|
+
# Let's assume POST to /remove or similar if DELETE body is flaky:
|
|
50
|
+
# We will use client.request method
|
|
51
|
+
result = client.request("DELETE", f"sessions/{session_id}/groups/{group_id}/participants", json=payload)
|
|
52
|
+
print_json_obj(result)
|
|
53
|
+
|
|
54
|
+
@app.command("update-participants")
|
|
55
|
+
def update_group_participants(ctx: typer.Context, session_id: str, group_id: str, action: str = typer.Option(..., help="promote|demote"), participants: str = typer.Option(..., help="Comma separated jids")):
|
|
56
|
+
client = get_client(ctx)
|
|
57
|
+
payload = {"action": action, "participants": participants.split(",")}
|
|
58
|
+
result = client.patch(f"sessions/{session_id}/groups/{group_id}/participants", json=payload)
|
|
59
|
+
print_json_obj(result)
|
|
60
|
+
|
|
61
|
+
@app.command("settings")
|
|
62
|
+
def update_group_settings(ctx: typer.Context, session_id: str, group_id: str, subject: Optional[str] = typer.Option(None, "--subject", "-s"), description: Optional[str] = typer.Option(None, "--description", "-d"), announce: Optional[bool] = typer.Option(None, "--announce", "-a")):
|
|
63
|
+
client = get_client(ctx)
|
|
64
|
+
payload = {}
|
|
65
|
+
if subject is not None:
|
|
66
|
+
payload["subject"] = subject
|
|
67
|
+
if description is not None:
|
|
68
|
+
payload["description"] = description
|
|
69
|
+
if announce is not None:
|
|
70
|
+
payload["announce"] = announce
|
|
71
|
+
result = client.patch(f"sessions/{session_id}/groups/{group_id}/settings", json=payload)
|
|
72
|
+
print_json_obj(result)
|
|
73
|
+
|
|
74
|
+
@app.command("leave")
|
|
75
|
+
def leave_group(ctx: typer.Context, session_id: str, group_id: str):
|
|
76
|
+
client = get_client(ctx)
|
|
77
|
+
result = client.post(f"sessions/{session_id}/groups/{group_id}/leave")
|
|
78
|
+
print_json_obj(result)
|
|
79
|
+
|
|
80
|
+
@app.command("invite-link")
|
|
81
|
+
def generate_invite_link(ctx: typer.Context, session_id: str, group_id: str):
|
|
82
|
+
client = get_client(ctx)
|
|
83
|
+
result = client.get(f"sessions/{session_id}/groups/{group_id}/invite-link")
|
|
84
|
+
print_json_obj(result)
|
|
85
|
+
|
|
86
|
+
@app.command("accept-invite")
|
|
87
|
+
def accept_group_invite(ctx: typer.Context, session_id: str, invite_code: str):
|
|
88
|
+
client = get_client(ctx)
|
|
89
|
+
result = client.post(f"sessions/{session_id}/groups/accept-invite/{invite_code}")
|
|
90
|
+
print_json_obj(result)
|
|
91
|
+
|
|
92
|
+
@app.command("search")
|
|
93
|
+
def search_groups(ctx: typer.Context, session_id: str, query: str):
|
|
94
|
+
client = get_client(ctx)
|
|
95
|
+
result = client.get(f"sessions/{session_id}/groups/search", params={"q": query})
|
|
96
|
+
print_json_obj(result)
|
wasender/main.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typer
|
|
3
|
+
from .output import set_output_mode, print_error_and_exit
|
|
4
|
+
from .client import WasenderClient
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
from . import sessions
|
|
9
|
+
from . import messages
|
|
10
|
+
from . import contacts
|
|
11
|
+
from . import groups
|
|
12
|
+
|
|
13
|
+
# Load environment variables from a .env file if it exists
|
|
14
|
+
load_dotenv()
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="WasenderAPI CLI Tool")
|
|
17
|
+
|
|
18
|
+
app.add_typer(sessions.app, name="sessions", help="Manage WhatsApp sessions")
|
|
19
|
+
app.add_typer(messages.app, name="messages", help="Send and manage messages")
|
|
20
|
+
app.add_typer(contacts.app, name="contacts", help="Manage contacts")
|
|
21
|
+
app.add_typer(groups.app, name="groups", help="Manage groups")
|
|
22
|
+
|
|
23
|
+
@app.callback()
|
|
24
|
+
def main(
|
|
25
|
+
ctx: typer.Context,
|
|
26
|
+
token: str = typer.Option(None, "--token", "-t", help="API Auth Token. Or use WASENDER_TOKEN env var.", envvar="WASENDER_TOKEN"),
|
|
27
|
+
base_url: str = typer.Option("https://wasenderapi.com/api", "--base-url", "-u", help="Base URL of the WasenderAPI.", envvar="WASENDER_BASE_URL"),
|
|
28
|
+
raw: bool = typer.Option(False, "--raw", help="Output raw JSON (for piping)"),
|
|
29
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress output, only exit codes")
|
|
30
|
+
):
|
|
31
|
+
set_output_mode(raw=raw, quiet=quiet)
|
|
32
|
+
|
|
33
|
+
if not token:
|
|
34
|
+
# Check env var directly in case typer envvar mapping has quirks
|
|
35
|
+
token = os.environ.get("WASENDER_TOKEN")
|
|
36
|
+
|
|
37
|
+
if not token and ctx.invoked_subcommand:
|
|
38
|
+
# Only require token if we are not asking for help or version
|
|
39
|
+
set_output_mode(False, False) # Show error even if quiet since we failed explicitly
|
|
40
|
+
print_error_and_exit("WASENDER_TOKEN environment variable or --token flag is required.")
|
|
41
|
+
|
|
42
|
+
ctx.obj = WasenderClient(token=token or "", base_url=base_url)
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
app()
|
wasender/messages.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from .output import print_json_obj
|
|
4
|
+
from .client import WasenderClient
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
app = typer.Typer()
|
|
8
|
+
|
|
9
|
+
def get_client(ctx: typer.Context) -> WasenderClient:
|
|
10
|
+
return ctx.obj
|
|
11
|
+
|
|
12
|
+
# Media helper for file uploading
|
|
13
|
+
def _upload_or_pass(client: WasenderClient, url_or_path: str):
|
|
14
|
+
# Depending on API, you might need multipart upload or just passing the URL.
|
|
15
|
+
# Wasender typically supports URL. For actual files, we use form upload if local.
|
|
16
|
+
if os.path.exists(url_or_path) and os.path.isfile(url_or_path):
|
|
17
|
+
import httpx
|
|
18
|
+
from .output import print_error_and_exit
|
|
19
|
+
try:
|
|
20
|
+
url = f"{client.base_url}/media/upload"
|
|
21
|
+
with open(url_or_path, "rb") as f:
|
|
22
|
+
try:
|
|
23
|
+
with httpx.Client() as c:
|
|
24
|
+
# Assuming an endpoint for file upload exists, if not we have to pass it in multipart
|
|
25
|
+
# Since WasenderAPI expects URLs or handles multipart in the same message endpoint,
|
|
26
|
+
pass
|
|
27
|
+
except Exception as e:
|
|
28
|
+
pass
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
# For now, we will pass it as a URL or assume the API handles it directly
|
|
32
|
+
return url_or_path
|
|
33
|
+
|
|
34
|
+
@app.command("text")
|
|
35
|
+
def send_text_message(ctx: typer.Context, session_id: str, to: str, message: str):
|
|
36
|
+
client = get_client(ctx)
|
|
37
|
+
payload = {"to": to, "text": message}
|
|
38
|
+
result = client.post(f"send-message", json=payload)
|
|
39
|
+
print_json_obj(result)
|
|
40
|
+
|
|
41
|
+
@app.command("mention")
|
|
42
|
+
def send_mention_message(ctx: typer.Context, session_id: str, to: str, message: str, mentions: str = typer.Option(..., help="Comma separated jids")):
|
|
43
|
+
client = get_client(ctx)
|
|
44
|
+
payload = {"to": to, "text": message, "mentions": mentions.split(",")}
|
|
45
|
+
result = client.post(f"sessions/{session_id}/messages/text", json=payload)
|
|
46
|
+
print_json_obj(result)
|
|
47
|
+
|
|
48
|
+
@app.command("image")
|
|
49
|
+
def send_image_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
|
|
50
|
+
client = get_client(ctx)
|
|
51
|
+
if os.path.isfile(url_or_path):
|
|
52
|
+
# We assume there's a multipart upload or Wasender accepts base64
|
|
53
|
+
# We will use multipart form data if it's a file
|
|
54
|
+
with open(url_or_path, "rb") as f:
|
|
55
|
+
files = {"file": f}
|
|
56
|
+
data = {"to": to}
|
|
57
|
+
result = client.request("POST", f"sessions/{session_id}/messages/image", data=data, files=files, headers={"Content-Type": None}) # Let httpx handle content type
|
|
58
|
+
print_json_obj(result)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
payload = {"to": to, "image": url_or_path}
|
|
62
|
+
result = client.post(f"sessions/{session_id}/messages/image", json=payload)
|
|
63
|
+
print_json_obj(result)
|
|
64
|
+
|
|
65
|
+
@app.command("audio")
|
|
66
|
+
def send_audio_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
|
|
67
|
+
client = get_client(ctx)
|
|
68
|
+
if os.path.isfile(url_or_path):
|
|
69
|
+
with open(url_or_path, "rb") as f:
|
|
70
|
+
files = {"file": f}
|
|
71
|
+
data = {"to": to}
|
|
72
|
+
result = client.request("POST", f"sessions/{session_id}/messages/audio", data=data, files=files, headers={"Content-Type": None})
|
|
73
|
+
print_json_obj(result)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
payload = {"to": to, "audio": url_or_path}
|
|
77
|
+
result = client.post(f"sessions/{session_id}/messages/audio", json=payload)
|
|
78
|
+
print_json_obj(result)
|
|
79
|
+
|
|
80
|
+
@app.command("video")
|
|
81
|
+
def send_video_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
|
|
82
|
+
client = get_client(ctx)
|
|
83
|
+
if os.path.isfile(url_or_path):
|
|
84
|
+
with open(url_or_path, "rb") as f:
|
|
85
|
+
files = {"file": f}
|
|
86
|
+
data = {"to": to}
|
|
87
|
+
result = client.request("POST", f"sessions/{session_id}/messages/video", data=data, files=files, headers={"Content-Type": None})
|
|
88
|
+
print_json_obj(result)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
payload = {"to": to, "video": url_or_path}
|
|
92
|
+
result = client.post(f"sessions/{session_id}/messages/video", json=payload)
|
|
93
|
+
print_json_obj(result)
|
|
94
|
+
|
|
95
|
+
@app.command("document")
|
|
96
|
+
def send_document_message(ctx: typer.Context, session_id: str, to: str, url_or_path: str):
|
|
97
|
+
client = get_client(ctx)
|
|
98
|
+
if os.path.isfile(url_or_path):
|
|
99
|
+
with open(url_or_path, "rb") as f:
|
|
100
|
+
files = {"file": f}
|
|
101
|
+
data = {"to": to}
|
|
102
|
+
result = client.request("POST", f"sessions/{session_id}/messages/document", data=data, files=files, headers={"Content-Type": None})
|
|
103
|
+
print_json_obj(result)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
payload = {"to": to, "document": url_or_path}
|
|
107
|
+
result = client.post(f"sessions/{session_id}/messages/document", json=payload)
|
|
108
|
+
print_json_obj(result)
|
|
109
|
+
|
|
110
|
+
@app.command("location")
|
|
111
|
+
def send_location_message(ctx: typer.Context, session_id: str, to: str, lat: float, lng: float):
|
|
112
|
+
client = get_client(ctx)
|
|
113
|
+
payload = {"to": to, "location": {"degreesLatitude": lat, "degreesLongitude": lng}}
|
|
114
|
+
result = client.post(f"sessions/{session_id}/messages/location", json=payload)
|
|
115
|
+
print_json_obj(result)
|
|
116
|
+
|
|
117
|
+
@app.command("contact")
|
|
118
|
+
def send_contact_message(ctx: typer.Context, session_id: str, to: str, vcard: str):
|
|
119
|
+
client = get_client(ctx)
|
|
120
|
+
payload = {"to": to, "vcard": vcard}
|
|
121
|
+
result = client.post(f"sessions/{session_id}/messages/contact", json=payload)
|
|
122
|
+
print_json_obj(result)
|
|
123
|
+
|
|
124
|
+
@app.command("poll")
|
|
125
|
+
def send_poll_message(ctx: typer.Context, session_id: str, to: str, question: str, options: str = typer.Option(..., help="Comma-separated options")):
|
|
126
|
+
client = get_client(ctx)
|
|
127
|
+
payload = {"to": to, "poll": {"name": question, "options": options.split(",")}}
|
|
128
|
+
result = client.post(f"sessions/{session_id}/messages/poll", json=payload)
|
|
129
|
+
print_json_obj(result)
|
|
130
|
+
|
|
131
|
+
@app.command("presence")
|
|
132
|
+
def send_presence_update(ctx: typer.Context, session_id: str, to: str, status: str = typer.Argument(..., help="available|composing|recording|paused")):
|
|
133
|
+
client = get_client(ctx)
|
|
134
|
+
payload = {"to": to, "presence": status}
|
|
135
|
+
result = client.post(f"sessions/{session_id}/messages/presence", json=payload)
|
|
136
|
+
print_json_obj(result)
|
|
137
|
+
|
|
138
|
+
@app.command("resend")
|
|
139
|
+
def resend_message(ctx: typer.Context, session_id: str, message_id: str):
|
|
140
|
+
client = get_client(ctx)
|
|
141
|
+
result = client.post(f"sessions/{session_id}/messages/{message_id}/resend")
|
|
142
|
+
print_json_obj(result)
|
|
143
|
+
|
|
144
|
+
@app.command("edit")
|
|
145
|
+
def edit_message(ctx: typer.Context, session_id: str, message_id: str, new_text: str):
|
|
146
|
+
client = get_client(ctx)
|
|
147
|
+
payload = {"text": new_text}
|
|
148
|
+
result = client.patch(f"sessions/{session_id}/messages/{message_id}", json=payload)
|
|
149
|
+
print_json_obj(result)
|
|
150
|
+
|
|
151
|
+
@app.command("delete")
|
|
152
|
+
def delete_message(ctx: typer.Context, session_id: str, message_id: str):
|
|
153
|
+
client = get_client(ctx)
|
|
154
|
+
result = client.delete(f"sessions/{session_id}/messages/{message_id}")
|
|
155
|
+
print_json_obj(result)
|
|
156
|
+
|
|
157
|
+
@app.command("info")
|
|
158
|
+
def get_message_info(ctx: typer.Context, session_id: str, message_id: str):
|
|
159
|
+
client = get_client(ctx)
|
|
160
|
+
result = client.get(f"sessions/{session_id}/messages/{message_id}")
|
|
161
|
+
print_json_obj(result)
|
wasender/output.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
console = Console()
|
|
5
|
+
|
|
6
|
+
_raw = False
|
|
7
|
+
_quiet = False
|
|
8
|
+
|
|
9
|
+
def set_output_mode(raw: bool, quiet: bool):
|
|
10
|
+
global _raw, _quiet
|
|
11
|
+
_raw = raw
|
|
12
|
+
_quiet = quiet
|
|
13
|
+
|
|
14
|
+
def print_json_obj(data):
|
|
15
|
+
if _quiet:
|
|
16
|
+
return
|
|
17
|
+
if _raw:
|
|
18
|
+
import json
|
|
19
|
+
print(json.dumps(data))
|
|
20
|
+
else:
|
|
21
|
+
console.print_json(data=data)
|
|
22
|
+
|
|
23
|
+
def print_error_and_exit(message: str, code: int = 1):
|
|
24
|
+
if not _quiet:
|
|
25
|
+
console.print(f"[bold red]Error[/bold red]: {message}")
|
|
26
|
+
sys.exit(code)
|
|
27
|
+
|
|
28
|
+
def print_http_error_and_exit(status_code: int, message: str):
|
|
29
|
+
if not _quiet:
|
|
30
|
+
console.print(f"[bold red]Error {status_code}[/bold red]: {message}")
|
|
31
|
+
sys.exit(1)
|
wasender/sessions.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import typer
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .output import print_json_obj
|
|
5
|
+
from .client import WasenderClient
|
|
6
|
+
|
|
7
|
+
app = typer.Typer()
|
|
8
|
+
|
|
9
|
+
def get_client(ctx: typer.Context) -> WasenderClient:
|
|
10
|
+
return ctx.obj
|
|
11
|
+
|
|
12
|
+
@app.command("list")
|
|
13
|
+
def list_sessions(ctx: typer.Context, page: int = 1, limit: int = 10):
|
|
14
|
+
client = get_client(ctx)
|
|
15
|
+
result = client.get("sessions", params={"page": page, "limit": limit})
|
|
16
|
+
print_json_obj(result)
|
|
17
|
+
|
|
18
|
+
@app.command("get")
|
|
19
|
+
def get_session(ctx: typer.Context, session_id: str):
|
|
20
|
+
client = get_client(ctx)
|
|
21
|
+
result = client.get(f"sessions/{session_id}")
|
|
22
|
+
print_json_obj(result)
|
|
23
|
+
|
|
24
|
+
@app.command("user")
|
|
25
|
+
def get_session_user(ctx: typer.Context, session_id: str):
|
|
26
|
+
client = get_client(ctx)
|
|
27
|
+
result = client.get(f"sessions/{session_id}/user")
|
|
28
|
+
print_json_obj(result)
|
|
29
|
+
|
|
30
|
+
@app.command("create")
|
|
31
|
+
def create_session(ctx: typer.Context, name: str = typer.Option(..., prompt=True), webhook: Optional[str] = None):
|
|
32
|
+
client = get_client(ctx)
|
|
33
|
+
payload = {"name": name}
|
|
34
|
+
if webhook:
|
|
35
|
+
payload["webhook"] = webhook
|
|
36
|
+
result = client.post("sessions", json=payload)
|
|
37
|
+
print_json_obj(result)
|
|
38
|
+
|
|
39
|
+
@app.command("update")
|
|
40
|
+
def update_session(ctx: typer.Context, session_id: str, name: Optional[str] = None, webhook: Optional[str] = None):
|
|
41
|
+
client = get_client(ctx)
|
|
42
|
+
payload = {}
|
|
43
|
+
if name is not None:
|
|
44
|
+
payload["name"] = name
|
|
45
|
+
if webhook is not None:
|
|
46
|
+
payload["webhook"] = webhook
|
|
47
|
+
result = client.patch(f"sessions/{session_id}", json=payload)
|
|
48
|
+
print_json_obj(result)
|
|
49
|
+
|
|
50
|
+
@app.command("connect")
|
|
51
|
+
def connect_session(ctx: typer.Context, session_id: str):
|
|
52
|
+
client = get_client(ctx)
|
|
53
|
+
result = client.post(f"sessions/{session_id}/connect")
|
|
54
|
+
print_json_obj(result)
|
|
55
|
+
|
|
56
|
+
@app.command("disconnect")
|
|
57
|
+
def disconnect_session(ctx: typer.Context, session_id: str):
|
|
58
|
+
client = get_client(ctx)
|
|
59
|
+
result = client.post(f"sessions/{session_id}/disconnect")
|
|
60
|
+
print_json_obj(result)
|
|
61
|
+
|
|
62
|
+
@app.command("message-logs")
|
|
63
|
+
def get_message_logs(ctx: typer.Context, session_id: str):
|
|
64
|
+
client = get_client(ctx)
|
|
65
|
+
result = client.get(f"sessions/{session_id}/message-logs")
|
|
66
|
+
print_json_obj(result)
|
|
67
|
+
|
|
68
|
+
@app.command("session-logs")
|
|
69
|
+
def get_session_logs(ctx: typer.Context, session_id: str):
|
|
70
|
+
client = get_client(ctx)
|
|
71
|
+
result = client.get(f"sessions/{session_id}/logs")
|
|
72
|
+
print_json_obj(result)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wasender-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A production-grade CLI tool for the WasenderAPI
|
|
5
|
+
Author-email: Tapatio Solutions <girishdudhwal@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: typer>=0.9.0
|
|
15
|
+
Requires-Dist: httpx>=0.25.0
|
|
16
|
+
Requires-Dist: rich>=13.0.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Wasender CLI
|
|
21
|
+
|
|
22
|
+
A production-grade CLI tool for the WasenderAPI using Python with Typer.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
1. Make sure you have Python 3.9+ installed.
|
|
27
|
+
2. Install the CLI in editable mode:
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
You must set your Wasender API token. You can pass it via `--token`, set an environment variable, or use a `.env` file.
|
|
35
|
+
|
|
36
|
+
**Option 1: `.env` file (Recommended)**
|
|
37
|
+
Create a `.env` file in the directory where you run the CLI:
|
|
38
|
+
```ini
|
|
39
|
+
WASENDER_TOKEN=your_token_here
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Option 2: Environment Variable**
|
|
43
|
+
```bash
|
|
44
|
+
export WASENDER_TOKEN=your_token_here
|
|
45
|
+
# On Windows PowerShell:
|
|
46
|
+
# $env:WASENDER_TOKEN="your_token_here"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
### Sessions Group
|
|
52
|
+
```bash
|
|
53
|
+
wasender sessions list
|
|
54
|
+
wasender sessions create --name "My Session"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Messages Group
|
|
58
|
+
```bash
|
|
59
|
+
wasender messages text my_session 1234567890 "Hello from Wasender CLI!"
|
|
60
|
+
wasender messages image my_session 1234567890 https://example.com/image.png
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Contacts Group
|
|
64
|
+
```bash
|
|
65
|
+
wasender contacts list my_session
|
|
66
|
+
wasender contacts add my_session 1234567890 "John Doe"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Groups Group
|
|
70
|
+
```bash
|
|
71
|
+
wasender groups list my_session
|
|
72
|
+
wasender groups create my_session "Test Group" --participants 1234567890@s.whatsapp.net,0987654321@s.whatsapp.net
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use `wasender --help` or `wasender <group> --help` to see all available commands and options.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
wasender/__init__.py,sha256=S9eLObttReEuA_m5THSNfyQrTbKLm40FyO8rfvYGLkk,49
|
|
2
|
+
wasender/client.py,sha256=y7Uh50ejidcXtvJdBLMR3rhxXh_IrInUIzV7sKxU9p8,2408
|
|
3
|
+
wasender/contacts.py,sha256=fT6ZuthZUqNA50JqAStdhR5mYpZRG0JLRWBSa9xeV6w,2450
|
|
4
|
+
wasender/groups.py,sha256=Cdpug5_FfcAtXd79UiucBn7xiwf37DBzUaeWzqyTuTk,4410
|
|
5
|
+
wasender/main.py,sha256=WxsYyYpECRWUaLt6oOILFZ38GE98CTkEKyIxjsPErY4,1767
|
|
6
|
+
wasender/messages.py,sha256=nnus0EcXNtjuQI7zEm7tlBhvz7bm71ctvCgTTLnIDDA,7011
|
|
7
|
+
wasender/output.py,sha256=BCPw4fOLkgKeUqzDHpJD4RcqGU4QjhvP8-4nr2Zy2ws,703
|
|
8
|
+
wasender/sessions.py,sha256=-e_bUS6zSEo3fWxOIWAyDaSQwcHdYg7u4U-KtRGpvQo,2357
|
|
9
|
+
wasender_cli-0.1.0.dist-info/licenses/LICENSE,sha256=77T62lnNdRZj0p9ri4jCBWdP3mKnY-z5R1WjtOgOgjk,1074
|
|
10
|
+
wasender_cli-0.1.0.dist-info/METADATA,sha256=8aEH6E8ezSaOiwAgUo7-ulFB_KQF3otlwqge0v3uh1g,2015
|
|
11
|
+
wasender_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
wasender_cli-0.1.0.dist-info/entry_points.txt,sha256=Q1VSEyDfTa04fHLne7uAAo-awfehw9Doa--iBTQEqYY,47
|
|
13
|
+
wasender_cli-0.1.0.dist-info/top_level.txt,sha256=pEMIDpGhre1TscVONhw6I9mVWgI2DQdJH9JXE3wKRv8,9
|
|
14
|
+
wasender_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tapatio Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wasender
|