xc-bot 1.0.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.
xc_bot/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """xc-bot - Python SDK for XaneoConnect Bot API"""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from .bot import Bot
6
+ from .grpc_client import GrpcBotClient
7
+ from .exceptions import (
8
+ XcBotError,
9
+ ApiError,
10
+ NetworkError,
11
+ RateLimitError,
12
+ WebhookConflictError,
13
+ )
14
+ from .types import (
15
+ User,
16
+ BotAccount,
17
+ Message,
18
+ Chat,
19
+ ChatType,
20
+ Update,
21
+ UpdateType,
22
+ )
23
+ from .webhook import WebhookServer
24
+
25
+ __all__ = [
26
+ "Bot",
27
+ "GrpcBotClient",
28
+ "WebhookServer",
29
+ "XcBotError",
30
+ "ApiError",
31
+ "NetworkError",
32
+ "RateLimitError",
33
+ "WebhookConflictError",
34
+ "User",
35
+ "BotAccount",
36
+ "Message",
37
+ "Chat",
38
+ "ChatType",
39
+ "Update",
40
+ "UpdateType",
41
+ ]
42
+
xc_bot/api.py ADDED
@@ -0,0 +1,175 @@
1
+ """Async API client for XaneoConnect Bot API"""
2
+ import asyncio
3
+ import logging
4
+ from typing import Any, Optional, TypeVar, Union
5
+
6
+ import aiohttp
7
+
8
+ from .exceptions import ApiError, NetworkError, RateLimitError, WebhookConflictError
9
+
10
+ T = TypeVar("T")
11
+
12
+ logger = logging.getLogger("xc_bot")
13
+
14
+
15
+ class ApiClient:
16
+ DEFAULT_BASE_URL = "https://xaneo.com/bot"
17
+ DEFAULT_TIMEOUT = 60
18
+ MAX_RETRIES = 3
19
+ RETRY_DELAYS = [1, 2, 4]
20
+
21
+ def __init__(
22
+ self,
23
+ token: str,
24
+ base_url: Optional[str] = None,
25
+ timeout: int = DEFAULT_TIMEOUT,
26
+ ):
27
+ self.token = token
28
+ self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
29
+ self.timeout = aiohttp.ClientTimeout(total=timeout)
30
+ self._session: Optional[aiohttp.ClientSession] = None
31
+ self._user_agent = "xc-bot-python/1.0"
32
+
33
+ async def _get_session(self) -> aiohttp.ClientSession:
34
+ if self._session is None or self._session.closed:
35
+ self._session = aiohttp.ClientSession(
36
+ timeout=self.timeout,
37
+ headers={
38
+ "Authorization": f"Bot {self.token}",
39
+ "User-Agent": self._user_agent,
40
+ "Content-Type": "application/json",
41
+ },
42
+ )
43
+ return self._session
44
+
45
+ async def close(self) -> None:
46
+ if self._session and not self._session.closed:
47
+ await self._session.close()
48
+
49
+ async def request(
50
+ self,
51
+ method: str,
52
+ data: Optional[dict] = None,
53
+ params: Optional[dict] = None,
54
+ ) -> Any:
55
+ url = f"{self.base_url}/{method}"
56
+ session = await self._get_session()
57
+
58
+ last_error: Optional[Exception] = None
59
+
60
+ for attempt in range(self.MAX_RETRIES):
61
+ try:
62
+ if data is not None:
63
+ response = await session.post(url, json=data)
64
+ elif params:
65
+ response = await session.get(url, params=params)
66
+ else:
67
+ response = await session.get(url)
68
+
69
+ async with response:
70
+ result = await self._parse_response(response, method)
71
+
72
+ if result is not None:
73
+ return result
74
+
75
+ except ApiError:
76
+ raise
77
+ except aiohttp.ClientError as e:
78
+ last_error = NetworkError(str(e))
79
+ logger.warning(f"Network error on {method} (attempt {attempt + 1}): {e}")
80
+ except Exception as e:
81
+ last_error = NetworkError(str(e))
82
+ logger.warning(f"Unexpected error on {method} (attempt {attempt + 1}): {e}")
83
+
84
+ if attempt < self.MAX_RETRIES - 1:
85
+ delay = self.RETRY_DELAYS[min(attempt, len(self.RETRY_DELAYS) - 1)]
86
+ logger.debug(f"Retrying {method} in {delay}s...")
87
+ await asyncio.sleep(delay)
88
+
89
+ raise last_error or NetworkError("Unknown error occurred")
90
+
91
+ async def _parse_response(self, response: aiohttp.ClientResponse, method: str) -> Any:
92
+ try:
93
+ json_data = await response.json()
94
+ except aiohttp.ContentTypeError:
95
+ text = await response.text()
96
+ raise NetworkError(f"Invalid JSON response: {text[:200]}")
97
+
98
+ if json_data.get("ok"):
99
+ return json_data.get("result")
100
+
101
+ error_code = json_data.get("error_code", response.status)
102
+ description = json_data.get("description") or json_data.get("detail") or "Unknown error"
103
+
104
+ if error_code == 429:
105
+ retry_after = response.headers.get("Retry-After")
106
+ try:
107
+ retry_after_seconds = float(retry_after) if retry_after else 1.0
108
+ except ValueError:
109
+ retry_after_seconds = 1.0
110
+ raise RateLimitError(description, retry_after_seconds)
111
+ if error_code == 409:
112
+ raise WebhookConflictError(description)
113
+ raise ApiError(error_code, description)
114
+
115
+ async def get_me(self) -> dict:
116
+ return await self.request("getMe")
117
+
118
+ async def get_updates(
119
+ self,
120
+ offset: int = 0,
121
+ limit: int = 100,
122
+ timeout: int = 30,
123
+ ) -> list:
124
+ return await self.request("getUpdates", {"offset": offset, "limit": limit, "timeout": timeout})
125
+
126
+ async def send_message(
127
+ self,
128
+ chat_id: str,
129
+ text: str,
130
+ reply_to_message_id: Optional[int] = None,
131
+ ) -> dict:
132
+ data: dict = {"chat_id": chat_id, "text": text}
133
+ if reply_to_message_id:
134
+ data["reply_to_message_id"] = reply_to_message_id
135
+ return await self.request("sendMessage", data)
136
+
137
+ async def edit_message(self, message_id: int, text: str) -> bool:
138
+ return await self.request("editMessage", {"message_id": message_id, "text": text})
139
+
140
+ async def delete_message(self, message_id: int) -> bool:
141
+ return await self.request("deleteMessage", {"message_id": message_id})
142
+
143
+ async def set_webhook(
144
+ self,
145
+ url: str,
146
+ secret: str = "",
147
+ allowed_updates: Optional[list] = None,
148
+ ) -> bool:
149
+ data: dict = {"url": url}
150
+ if secret:
151
+ data["secret"] = secret
152
+ if allowed_updates:
153
+ data["allowed_updates"] = allowed_updates
154
+ return await self.request("setWebhook", data)
155
+
156
+ async def delete_webhook(self) -> bool:
157
+ return await self.request("deleteWebhook", {})
158
+
159
+ async def get_webhook_info(self) -> dict:
160
+ return await self.request("getWebhookInfo")
161
+
162
+ async def get_chat(self, chat_id: str) -> dict:
163
+ return await self.request("getChat", params={"chat_id": chat_id})
164
+
165
+ async def get_chat_members(self, chat_id: str) -> list:
166
+ return await self.request("getChatMembers", params={"chat_id": chat_id})
167
+
168
+ async def leave_chat(self, chat_id: str) -> bool:
169
+ return await self.request("leaveChat", {"chat_id": chat_id})
170
+
171
+ async def set_commands(self, commands: list) -> bool:
172
+ return await self.request("setCommands", {"commands": commands})
173
+
174
+ async def get_commands(self) -> list:
175
+ return await self.request("getCommands")
xc_bot/bot.py ADDED
@@ -0,0 +1,156 @@
1
+ """Main Bot class for xc-bot"""
2
+ import asyncio
3
+ import logging
4
+ import signal
5
+ from typing import Any, Callable, List, Optional, Union
6
+
7
+ from .api import ApiClient
8
+ from .exceptions import RateLimitError, XcBotError
9
+ from .handlers import HandlerRegistry
10
+ from .types import BotAccount, Chat, Message, Update
11
+
12
+ logger = logging.getLogger("xc_bot")
13
+
14
+
15
+ class Bot:
16
+ def __init__(
17
+ self,
18
+ token: str,
19
+ base_url: Optional[str] = None,
20
+ ):
21
+ self.token = token
22
+ self._api = ApiClient(token, base_url)
23
+ self._handlers = HandlerRegistry()
24
+ self._running = False
25
+ self._me: Optional[BotAccount] = None
26
+
27
+ @property
28
+ def api(self) -> ApiClient:
29
+ return self._api
30
+
31
+ async def get_me(self) -> BotAccount:
32
+ data = await self._api.get_me()
33
+ self._me = BotAccount.from_dict(data)
34
+ return self._me
35
+
36
+ async def send_message(
37
+ self,
38
+ chat_id: str,
39
+ text: str,
40
+ reply_to_message_id: Optional[int] = None,
41
+ ) -> Message:
42
+ data = await self._api.send_message(chat_id, text, reply_to_message_id)
43
+ author = {
44
+ "id": self._me.id if self._me else 0,
45
+ "username": self._me.username if self._me else None,
46
+ "first_name": self._me.display_name if self._me else None,
47
+ }
48
+ return Message.from_dict(
49
+ {"id": data["message_id"], "chat_id": data["chat_id"], "text": text, "author": author},
50
+ bot=self,
51
+ )
52
+
53
+ async def edit_message(self, message_id: int, text: str) -> bool:
54
+ return await self._api.edit_message(message_id, text)
55
+
56
+ async def delete_message(self, message_id: int) -> bool:
57
+ return await self._api.delete_message(message_id)
58
+
59
+ async def get_chat(self, chat_id: str) -> Chat:
60
+ data = await self._api.get_chat(chat_id)
61
+ return Chat.from_dict(data)
62
+
63
+ async def get_chat_members(self, chat_id: str) -> List[dict]:
64
+ return await self._api.get_chat_members(chat_id)
65
+
66
+ async def leave_chat(self, chat_id: str) -> bool:
67
+ return await self._api.leave_chat(chat_id)
68
+
69
+ async def set_commands(self, commands: List[dict]) -> bool:
70
+ return await self._api.set_commands(commands)
71
+
72
+ async def get_commands(self) -> List[dict]:
73
+ return await self._api.get_commands()
74
+
75
+ async def set_webhook(self, url: str, secret: str = "", allowed_updates: Optional[List[str]] = None) -> bool:
76
+ return await self._api.set_webhook(url, secret, allowed_updates)
77
+
78
+ async def delete_webhook(self) -> bool:
79
+ return await self._api.delete_webhook()
80
+
81
+ async def get_webhook_info(self) -> dict:
82
+ return await self._api.get_webhook_info()
83
+
84
+ def command(self, commands: Union[str, List[str]]) -> Callable:
85
+ def decorator(func: Callable) -> Callable:
86
+ self._handlers.add_command(commands, func)
87
+ return func
88
+ return decorator
89
+
90
+ def hears(self, pattern: Union[str, Any]) -> Callable:
91
+ def decorator(func: Callable) -> Callable:
92
+ self._handlers.add_regex(pattern, func)
93
+ return func
94
+ return decorator
95
+
96
+ def message(self, func: Callable) -> Callable:
97
+ self._handlers.add_message(func)
98
+ return func
99
+
100
+ async def _process_update(self, update_data: dict) -> None:
101
+ update = Update.from_dict(update_data, bot=self)
102
+ if update.message:
103
+ update.message._bot = self
104
+ try:
105
+ await self._handlers.process(update.message)
106
+ except Exception as e:
107
+ logger.exception(f"Error processing update {update.update_id}: {e}")
108
+
109
+ async def polling(self, timeout: int = 30, limit: int = 100) -> None:
110
+ self._running = True
111
+ offset = 0
112
+
113
+ def signal_handler(sig, frame):
114
+ nonlocal self
115
+ logger.info("Received shutdown signal")
116
+ self._running = False
117
+
118
+ signal.signal(signal.SIGINT, signal_handler)
119
+ signal.signal(signal.SIGTERM, signal_handler)
120
+
121
+ logger.info("Starting polling...")
122
+
123
+ while self._running:
124
+ try:
125
+ updates = await self._api.get_updates(offset=offset, limit=limit, timeout=timeout)
126
+
127
+ if not updates:
128
+ await asyncio.sleep(1.1)
129
+ continue
130
+
131
+ for update_data in updates:
132
+ if not self._running:
133
+ break
134
+ await self._process_update(update_data)
135
+ offset = update_data.get("update_id", 0) + 1
136
+
137
+ except RateLimitError as e:
138
+ logger.info(f"Rate limited: {e.description}")
139
+ await asyncio.sleep(max(e.retry_after, 1.0) + 0.25)
140
+ except XcBotError as e:
141
+ logger.error(f"Bot error: {e}")
142
+ await asyncio.sleep(5)
143
+ except Exception as e:
144
+ logger.exception(f"Unexpected error in polling: {e}")
145
+ await asyncio.sleep(5)
146
+
147
+ logger.info("Polling stopped")
148
+
149
+ def run(self, timeout: int = 30, limit: int = 100) -> None:
150
+ try:
151
+ asyncio.run(self.polling(timeout=timeout, limit=limit))
152
+ except KeyboardInterrupt:
153
+ logger.info("Bot stopped by user")
154
+
155
+ async def close(self) -> None:
156
+ await self._api.close()
xc_bot/exceptions.py ADDED
@@ -0,0 +1,33 @@
1
+ """xc-bot exceptions"""
2
+
3
+
4
+ class XcBotError(Exception):
5
+ """Base exception for xc-bot"""
6
+
7
+
8
+ class ApiError(XcBotError):
9
+ """API returned an error"""
10
+
11
+ def __init__(self, error_code: int, description: str):
12
+ self.error_code = error_code
13
+ self.description = description
14
+ super().__init__(f"API error {error_code}: {description}")
15
+
16
+
17
+ class NetworkError(XcBotError):
18
+ """Network-related error"""
19
+
20
+
21
+ class RateLimitError(ApiError):
22
+ """Rate limit exceeded (429)"""
23
+
24
+ def __init__(self, description: str = "Too many requests", retry_after: float = 1.0):
25
+ self.retry_after = retry_after
26
+ super().__init__(429, description)
27
+
28
+
29
+ class WebhookConflictError(ApiError):
30
+ """Cannot use getUpdates while webhook is active (409)"""
31
+
32
+ def __init__(self, description: str = "Cannot use getUpdates while webhook is active"):
33
+ super().__init__(409, description)
xc_bot/grpc_client.py ADDED
@@ -0,0 +1,59 @@
1
+ import time
2
+ import logging
3
+ import grpc
4
+
5
+ try:
6
+ from .pb import bot_service_pb2 as pb2
7
+ from .pb import bot_service_pb2_grpc as pb2_grpc
8
+ except ImportError:
9
+ import bot_service_pb2 as pb2
10
+ import bot_service_pb2_grpc as pb2_grpc
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class GrpcBotClient:
16
+ """
17
+ gRPC High-Performance Client for Xaneo Bot SDK (xc-bot).
18
+ Allows real-time streaming of events and sub-millisecond message dispatching.
19
+ """
20
+
21
+ def __init__(self, bot_token: str, server_address: str = "localhost:50052"):
22
+ self.bot_token = bot_token
23
+ self.server_address = server_address
24
+ self.channel = None
25
+ self.stub = None
26
+
27
+ def connect(self):
28
+ """Establish gRPC connection to Xaneo Bot Platform."""
29
+ logger.info(f"Connecting to Xaneo Bot gRPC Engine at {self.server_address}...")
30
+ self.channel = grpc.insecure_channel(self.server_address)
31
+ self.stub = pb2_grpc.BotEventServiceStub(self.channel)
32
+ logger.info("✅ gRPC Bot Client Connected Successfully!")
33
+
34
+ def send_message(self, chat_id: str, text: str, reply_to_id: str = ""):
35
+ """Send message via gRPC Unary RPC."""
36
+ if not self.stub:
37
+ self.connect()
38
+
39
+ try:
40
+ req = pb2.BotSendMessageRequest(
41
+ bot_token=self.bot_token,
42
+ chat_id=str(chat_id),
43
+ text=text,
44
+ reply_to_id=reply_to_id
45
+ )
46
+ response = self.stub.SendMessage(req)
47
+ return {
48
+ "success": response.success,
49
+ "message_id": response.message_id,
50
+ "timestamp": response.timestamp,
51
+ "error": response.error_message
52
+ }
53
+ except Exception as e:
54
+ logger.error(f"Error sending message over gRPC: {e}")
55
+ return {"success": False, "error": str(e)}
56
+
57
+ def close(self):
58
+ if self.channel:
59
+ self.channel.close()
@@ -0,0 +1,4 @@
1
+ """Handlers package"""
2
+ from .handler import HandlerRegistry, HandlerEntry
3
+
4
+ __all__ = ["HandlerRegistry", "HandlerEntry"]
@@ -0,0 +1,84 @@
1
+ """Handler system for xc-bot"""
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Callable, Dict, List, Optional, Pattern, Union
5
+
6
+ from xc_bot.types import Message
7
+
8
+
9
+ @dataclass
10
+ class HandlerEntry:
11
+ handler: Callable
12
+ priority: int = 0
13
+ pattern: Optional[Pattern] = None
14
+ commands: Optional[List[str]] = None
15
+
16
+
17
+ class HandlerRegistry:
18
+ def __init__(self):
19
+ self._command_handlers: Dict[str, List[HandlerEntry]] = {}
20
+ self._regex_handlers: List[HandlerEntry] = []
21
+ self._message_handlers: List[HandlerEntry] = []
22
+
23
+ def add_command(self, commands: Union[str, List[str]], handler: Callable) -> None:
24
+ if isinstance(commands, str):
25
+ commands = [commands]
26
+
27
+ for cmd in commands:
28
+ cmd_clean = cmd.lstrip("/").lower()
29
+ if cmd_clean not in self._command_handlers:
30
+ self._command_handlers[cmd_clean] = []
31
+ self._command_handlers[cmd_clean].append(HandlerEntry(handler=handler))
32
+
33
+ def add_regex(self, pattern: Union[str, Pattern], handler: Callable) -> None:
34
+ if isinstance(pattern, str):
35
+ compiled = re.compile(pattern)
36
+ else:
37
+ compiled = pattern
38
+ self._regex_handlers.append(HandlerEntry(handler=handler, pattern=compiled))
39
+
40
+ def add_message(self, handler: Callable) -> None:
41
+ self._message_handlers.append(HandlerEntry(handler=handler))
42
+
43
+ async def process(self, message: Message) -> bool:
44
+ text = message.text or ""
45
+
46
+ if text.startswith("/"):
47
+ parts = text.split(None, 1)
48
+ command = parts[0].lstrip("/").lower()
49
+
50
+ if "@" in command:
51
+ command = command.split("@")[0]
52
+
53
+ handlers = self._command_handlers.get(command, [])
54
+ for entry in handlers:
55
+ try:
56
+ result = entry.handler(message)
57
+ if result is not None and hasattr(result, "__await__"):
58
+ await result
59
+ return True
60
+ except Exception:
61
+ continue
62
+
63
+ for entry in self._regex_handlers:
64
+ if entry.pattern:
65
+ match = entry.pattern.search(text)
66
+ if match:
67
+ try:
68
+ result = entry.handler(message, match)
69
+ if result is not None and hasattr(result, "__await__"):
70
+ await result
71
+ return True
72
+ except Exception:
73
+ continue
74
+
75
+ for entry in self._message_handlers:
76
+ try:
77
+ result = entry.handler(message)
78
+ if result is not None and hasattr(result, "__await__"):
79
+ await result
80
+ return True
81
+ except Exception:
82
+ continue
83
+
84
+ return False
xc_bot/pb/__init__.py ADDED
File without changes
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: bot_service.proto
5
+ # Protobuf Python Version: 7.35.0
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 7,
15
+ 35,
16
+ 0,
17
+ '',
18
+ 'bot_service.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x62ot_service.proto\x12\x08xaneo.v1\"v\n\x15\x42otSendMessageRequest\x12\x11\n\tbot_token\x18\x01 \x01(\t\x12\x0f\n\x07\x63hat_id\x18\x02 \x01(\t\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x13\n\x0breply_to_id\x18\x04 \x01(\t\x12\x16\n\x0einline_buttons\x18\x05 \x03(\t\"g\n\x16\x42otSendMessageResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x12\n\nmessage_id\x18\x02 \x01(\t\x12\x15\n\rerror_message\x18\x03 \x01(\t\x12\x11\n\ttimestamp\x18\x04 \x01(\x03\"\xb5\x01\n\x08\x42otEvent\x12\x10\n\x08\x65vent_id\x18\x01 \x01(\t\x12\x12\n\nevent_type\x18\x02 \x01(\t\x12\x0e\n\x06\x62ot_id\x18\x03 \x01(\t\x12\x0f\n\x07\x63hat_id\x18\x04 \x01(\t\x12\x11\n\tsender_id\x18\x05 \x01(\t\x12\x17\n\x0fsender_username\x18\x06 \x01(\t\x12\x0c\n\x04text\x18\x07 \x01(\t\x12\x15\n\rcallback_data\x18\x08 \x01(\t\x12\x11\n\ttimestamp\x18\t \x01(\x03\"q\n\tBotAction\x12\x11\n\tbot_token\x18\x01 \x01(\t\x12\x10\n\x08\x65vent_id\x18\x02 \x01(\t\x12\x13\n\x0b\x61\x63tion_type\x18\x03 \x01(\t\x12\x12\n\nreply_text\x18\x04 \x01(\t\x12\x16\n\x0einline_buttons\x18\x05 \x03(\t2\xa0\x01\n\x0f\x42otEventService\x12;\n\x0cStreamEvents\x12\x13.xaneo.v1.BotAction\x1a\x12.xaneo.v1.BotEvent(\x01\x30\x01\x12P\n\x0bSendMessage\x12\x1f.xaneo.v1.BotSendMessageRequest\x1a .xaneo.v1.BotSendMessageResponseb\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'bot_service_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_BOTSENDMESSAGEREQUEST']._serialized_start=31
35
+ _globals['_BOTSENDMESSAGEREQUEST']._serialized_end=149
36
+ _globals['_BOTSENDMESSAGERESPONSE']._serialized_start=151
37
+ _globals['_BOTSENDMESSAGERESPONSE']._serialized_end=254
38
+ _globals['_BOTEVENT']._serialized_start=257
39
+ _globals['_BOTEVENT']._serialized_end=438
40
+ _globals['_BOTACTION']._serialized_start=440
41
+ _globals['_BOTACTION']._serialized_end=553
42
+ _globals['_BOTEVENTSERVICE']._serialized_start=556
43
+ _globals['_BOTEVENTSERVICE']._serialized_end=716
44
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,145 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+ import bot_service_pb2 as bot__service__pb2
7
+
8
+ GRPC_GENERATED_VERSION = '1.82.1'
9
+ GRPC_VERSION = grpc.__version__
10
+ _version_not_supported = False
11
+
12
+ try:
13
+ from grpc._utilities import first_version_is_lower
14
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
15
+ except ImportError:
16
+ _version_not_supported = True
17
+
18
+ if _version_not_supported:
19
+ raise RuntimeError(
20
+ f'The grpc package installed is at version {GRPC_VERSION},'
21
+ + ' but the generated code in bot_service_pb2_grpc.py depends on'
22
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
23
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
24
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
25
+ )
26
+
27
+
28
+ class BotEventServiceStub:
29
+ """Service for high-performance Bot Platform real-time communication
30
+ """
31
+
32
+ def __init__(self, channel):
33
+ """Constructor.
34
+
35
+ Args:
36
+ channel: A grpc.Channel.
37
+ """
38
+ self.StreamEvents = channel.stream_stream(
39
+ '/xaneo.v1.BotEventService/StreamEvents',
40
+ request_serializer=bot__service__pb2.BotAction.SerializeToString,
41
+ response_deserializer=bot__service__pb2.BotEvent.FromString,
42
+ _registered_method=True)
43
+ self.SendMessage = channel.unary_unary(
44
+ '/xaneo.v1.BotEventService/SendMessage',
45
+ request_serializer=bot__service__pb2.BotSendMessageRequest.SerializeToString,
46
+ response_deserializer=bot__service__pb2.BotSendMessageResponse.FromString,
47
+ _registered_method=True)
48
+
49
+
50
+ class BotEventServiceServicer:
51
+ """Service for high-performance Bot Platform real-time communication
52
+ """
53
+
54
+ def StreamEvents(self, request_iterator, context):
55
+ """Bi-directional stream for bot events and real-time bot responses
56
+ """
57
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
58
+ context.set_details('Method not implemented!')
59
+ raise NotImplementedError('Method not implemented!')
60
+
61
+ def SendMessage(self, request, context):
62
+ """Unary call for fast message sending by bots
63
+ """
64
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
65
+ context.set_details('Method not implemented!')
66
+ raise NotImplementedError('Method not implemented!')
67
+
68
+
69
+ def add_BotEventServiceServicer_to_server(servicer, server):
70
+ rpc_method_handlers = {
71
+ 'StreamEvents': grpc.stream_stream_rpc_method_handler(
72
+ servicer.StreamEvents,
73
+ request_deserializer=bot__service__pb2.BotAction.FromString,
74
+ response_serializer=bot__service__pb2.BotEvent.SerializeToString,
75
+ ),
76
+ 'SendMessage': grpc.unary_unary_rpc_method_handler(
77
+ servicer.SendMessage,
78
+ request_deserializer=bot__service__pb2.BotSendMessageRequest.FromString,
79
+ response_serializer=bot__service__pb2.BotSendMessageResponse.SerializeToString,
80
+ ),
81
+ }
82
+ generic_handler = grpc.method_handlers_generic_handler(
83
+ 'xaneo.v1.BotEventService', rpc_method_handlers)
84
+ server.add_generic_rpc_handlers((generic_handler,))
85
+ server.add_registered_method_handlers('xaneo.v1.BotEventService', rpc_method_handlers)
86
+
87
+
88
+ # This class is part of an EXPERIMENTAL API.
89
+ class BotEventService:
90
+ """Service for high-performance Bot Platform real-time communication
91
+ """
92
+
93
+ @staticmethod
94
+ def StreamEvents(request_iterator,
95
+ target,
96
+ options=(),
97
+ channel_credentials=None,
98
+ call_credentials=None,
99
+ insecure=False,
100
+ compression=None,
101
+ wait_for_ready=None,
102
+ timeout=None,
103
+ metadata=None):
104
+ return grpc.experimental.stream_stream(
105
+ request_iterator,
106
+ target,
107
+ '/xaneo.v1.BotEventService/StreamEvents',
108
+ bot__service__pb2.BotAction.SerializeToString,
109
+ bot__service__pb2.BotEvent.FromString,
110
+ options,
111
+ channel_credentials,
112
+ insecure,
113
+ call_credentials,
114
+ compression,
115
+ wait_for_ready,
116
+ timeout,
117
+ metadata,
118
+ _registered_method=True)
119
+
120
+ @staticmethod
121
+ def SendMessage(request,
122
+ target,
123
+ options=(),
124
+ channel_credentials=None,
125
+ call_credentials=None,
126
+ insecure=False,
127
+ compression=None,
128
+ wait_for_ready=None,
129
+ timeout=None,
130
+ metadata=None):
131
+ return grpc.experimental.unary_unary(
132
+ request,
133
+ target,
134
+ '/xaneo.v1.BotEventService/SendMessage',
135
+ bot__service__pb2.BotSendMessageRequest.SerializeToString,
136
+ bot__service__pb2.BotSendMessageResponse.FromString,
137
+ options,
138
+ channel_credentials,
139
+ insecure,
140
+ call_credentials,
141
+ compression,
142
+ wait_for_ready,
143
+ timeout,
144
+ metadata,
145
+ _registered_method=True)
@@ -0,0 +1,14 @@
1
+ """xc-bot types"""
2
+ from .user import User, BotAccount
3
+ from .message import Message, Chat, ChatType
4
+ from .update import Update, UpdateType
5
+
6
+ __all__ = [
7
+ "User",
8
+ "BotAccount",
9
+ "Message",
10
+ "Chat",
11
+ "ChatType",
12
+ "Update",
13
+ "UpdateType",
14
+ ]
@@ -0,0 +1,98 @@
1
+ """Message and Chat types"""
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Any, Optional
6
+
7
+ if TYPE_CHECKING:
8
+ from xc_bot.bot import Bot
9
+
10
+ from .user import User
11
+
12
+
13
+ class ChatType(Enum):
14
+ PRIVATE = "private"
15
+ GROUP = "group"
16
+ CHANNEL = "channel"
17
+ UNKNOWN = "unknown"
18
+
19
+
20
+ @dataclass
21
+ class Chat:
22
+ id: str
23
+ type: ChatType
24
+ title: Optional[str] = None
25
+ member_count: Optional[int] = None
26
+ subscriber_count: Optional[int] = None
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: dict) -> "Chat":
30
+ chat_type = data.get("type", "unknown")
31
+ try:
32
+ chat_type_enum = ChatType(chat_type)
33
+ except ValueError:
34
+ chat_type_enum = ChatType.UNKNOWN
35
+
36
+ return cls(
37
+ id=data["chat_id"],
38
+ type=chat_type_enum,
39
+ title=data.get("title"),
40
+ member_count=data.get("member_count"),
41
+ subscriber_count=data.get("subscriber_count"),
42
+ )
43
+
44
+
45
+ @dataclass
46
+ class Message:
47
+ id: int
48
+ chat_id: str
49
+ author: User
50
+ text: str
51
+ created_at: datetime
52
+ reply_to: Optional[int] = None
53
+ is_bot_message: bool = False
54
+ _bot: Optional["Bot"] = field(default=None, repr=False)
55
+
56
+ @classmethod
57
+ def from_dict(cls, data: dict, bot: Optional["Bot"] = None) -> "Message":
58
+ author_data = data.get("author", {})
59
+ if isinstance(author_data, dict):
60
+ author = User.from_dict(author_data)
61
+ else:
62
+ author = User(id=data.get("author_id", 0), username=data.get("author_username"))
63
+
64
+ created_at = data.get("created_at")
65
+ if isinstance(created_at, str):
66
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
67
+ elif isinstance(created_at, (int, float)):
68
+ created_at = datetime.fromtimestamp(created_at)
69
+ else:
70
+ created_at = datetime.now()
71
+
72
+ return cls(
73
+ id=data["id"],
74
+ chat_id=data["chat_id"],
75
+ author=author,
76
+ text=data.get("text", ""),
77
+ created_at=created_at,
78
+ reply_to=data.get("reply_to_id") or data.get("reply_to"),
79
+ is_bot_message=data.get("is_bot_message", False),
80
+ _bot=bot,
81
+ )
82
+
83
+ async def reply(self, text: str, **kwargs) -> "Message":
84
+ if self._bot is None:
85
+ raise RuntimeError("Message object not linked to Bot instance")
86
+ return await self._bot.send_message(
87
+ self.chat_id, text, reply_to_message_id=self.id, **kwargs
88
+ )
89
+
90
+ async def edit(self, text: str) -> bool:
91
+ if self._bot is None:
92
+ raise RuntimeError("Message object not linked to Bot instance")
93
+ return await self._bot.edit_message(self.id, text)
94
+
95
+ async def delete(self) -> bool:
96
+ if self._bot is None:
97
+ raise RuntimeError("Message object not linked to Bot instance")
98
+ return await self._bot.delete_message(self.id)
xc_bot/types/update.py ADDED
@@ -0,0 +1,46 @@
1
+ """Update types"""
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from typing import Any, Optional, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from xc_bot.bot import Bot
8
+
9
+ from .message import Message
10
+
11
+
12
+ class UpdateType(Enum):
13
+ MESSAGE = "message"
14
+ EDITED_MESSAGE = "edited_message"
15
+ DELETED_MESSAGE = "deleted_message"
16
+ CALLBACK_QUERY = "callback_query"
17
+ CHAT_MEMBER = "chat_member"
18
+ UNKNOWN = "unknown"
19
+
20
+
21
+ @dataclass
22
+ class Update:
23
+ update_id: int
24
+ update_type: UpdateType
25
+ message: Optional[Message] = None
26
+ data: dict = None
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: dict, bot: Optional["Bot"] = None) -> "Update":
30
+ update_type_str = data.get("update_type", "unknown")
31
+ try:
32
+ update_type = UpdateType(update_type_str)
33
+ except ValueError:
34
+ update_type = UpdateType.UNKNOWN
35
+
36
+ message = None
37
+ message_data = data.get("message")
38
+ if message_data:
39
+ message = Message.from_dict(message_data, bot=bot)
40
+
41
+ return cls(
42
+ update_id=data["update_id"],
43
+ update_type=update_type,
44
+ message=message,
45
+ data=data,
46
+ )
xc_bot/types/user.py ADDED
@@ -0,0 +1,42 @@
1
+ """User types"""
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass
8
+ class User:
9
+ id: int
10
+ username: Optional[str] = None
11
+ first_name: Optional[str] = None
12
+
13
+ @classmethod
14
+ def from_dict(cls, data: dict) -> "User":
15
+ return cls(
16
+ id=data["id"],
17
+ username=data.get("username"),
18
+ first_name=data.get("first_name"),
19
+ )
20
+
21
+
22
+ @dataclass
23
+ class BotAccount:
24
+ id: int
25
+ username: str
26
+ display_name: str
27
+ description: str = ""
28
+ can_join_groups: bool = True
29
+ can_read_all_messages: bool = False
30
+ inline_mode: bool = False
31
+
32
+ @classmethod
33
+ def from_dict(cls, data: dict) -> "BotAccount":
34
+ return cls(
35
+ id=data["id"],
36
+ username=data["username"],
37
+ display_name=data.get("display_name", ""),
38
+ description=data.get("description", ""),
39
+ can_join_groups=data.get("can_join_groups", True),
40
+ can_read_all_messages=data.get("can_read_all_messages", False),
41
+ inline_mode=data.get("inline_mode", False),
42
+ )
@@ -0,0 +1,4 @@
1
+ """Webhook package"""
2
+ from .server import WebhookServer
3
+
4
+ __all__ = ["WebhookServer"]
@@ -0,0 +1,99 @@
1
+ """Webhook server for xc-bot"""
2
+ import hashlib
3
+ import hmac
4
+ import json
5
+ import logging
6
+ from typing import Callable, Optional
7
+
8
+ from aiohttp import web
9
+
10
+ logger = logging.getLogger("xc_bot.webhook")
11
+
12
+
13
+ class WebhookServer:
14
+ def __init__(
15
+ self,
16
+ bot,
17
+ path: str = "/webhook",
18
+ host: str = "0.0.0.0",
19
+ port: int = 8443,
20
+ secret: Optional[str] = None,
21
+ ):
22
+ self.bot = bot
23
+ self.path = path
24
+ self.host = host
25
+ self.port = port
26
+ self.secret = secret
27
+ self._app: Optional[web.Application] = None
28
+ self._runner: Optional[web.AppRunner] = None
29
+
30
+ def _verify_signature(self, payload: bytes, signature: str) -> bool:
31
+ if not self.secret:
32
+ return True
33
+
34
+ if not signature.startswith("sha256="):
35
+ return False
36
+
37
+ expected_sig = signature[7:]
38
+ computed_sig = hmac.new(
39
+ self.secret.encode(),
40
+ payload,
41
+ hashlib.sha256,
42
+ ).hexdigest()
43
+
44
+ return hmac.compare_digest(expected_sig, computed_sig)
45
+
46
+ async def _handle_webhook(self, request: web.Request) -> web.Response:
47
+ signature = request.headers.get("X-XC-Signature", "")
48
+
49
+ payload = await request.read()
50
+
51
+ if not self._verify_signature(payload, signature):
52
+ logger.warning("Invalid webhook signature")
53
+ return web.Response(status=401, text="Unauthorized")
54
+
55
+ try:
56
+ data = json.loads(payload)
57
+ except json.JSONDecodeError:
58
+ logger.warning("Invalid JSON in webhook payload")
59
+ return web.Response(status=400, text="Bad Request")
60
+
61
+ try:
62
+ await self.bot._process_update(data)
63
+ except Exception as e:
64
+ logger.exception(f"Error processing webhook update: {e}")
65
+ return web.Response(status=500, text="Internal Server Error")
66
+
67
+ return web.Response(text="OK")
68
+
69
+ async def start(self) -> None:
70
+ self._app = web.Application()
71
+ self._app.router.add_post(self.path, self._handle_webhook)
72
+
73
+ self._runner = web.AppRunner(self._app)
74
+ await self._runner.setup()
75
+
76
+ site = web.TCPSite(self._runner, self.host, self.port)
77
+ await site.start()
78
+
79
+ logger.info(f"Webhook server started on http://{self.host}:{self.port}{self.path}")
80
+
81
+ async def stop(self) -> None:
82
+ if self._runner:
83
+ await self._runner.cleanup()
84
+ await self.bot.close()
85
+
86
+ def run(self) -> None:
87
+ import asyncio
88
+
89
+ loop = asyncio.new_event_loop()
90
+ asyncio.set_event_loop(loop)
91
+
92
+ try:
93
+ loop.run_until_complete(self.start())
94
+ loop.run_forever()
95
+ except KeyboardInterrupt:
96
+ pass
97
+ finally:
98
+ loop.run_until_complete(self.stop())
99
+ loop.close()
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: xc-bot
3
+ Version: 1.0.0
4
+ Summary: Python SDK for XaneoConnect Bot API
5
+ Author: Xaneo Team
6
+ Author-email: Xaneo Team <ltpddwk@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/saneome2/xc-bot
9
+ Project-URL: Documentation, https://xaneo.com/bot-api
10
+ Project-URL: Repository, https://github.com/saneome2/xc-bot
11
+ Keywords: xaneo,bot,api,sdk,async
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Framework :: AsyncIO
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: aiohttp>=3.8.0
26
+ Requires-Dist: grpcio>=1.60.0
27
+ Requires-Dist: protobuf>=4.25.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
31
+ Requires-Dist: aioresponses>=0.7.0; extra == "dev"
32
+ Requires-Dist: black>=23.0.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
+ Dynamic: author
35
+ Dynamic: license-file
36
+ Dynamic: requires-python
37
+
38
+ # xc-bot
39
+
40
+ Python SDK for XaneoConnect Bot API
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install xc-bot
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```python
51
+ from xc_bot import Bot
52
+
53
+ bot = Bot("xbot_12345_abc...")
54
+
55
+ @bot.command('start')
56
+ async def start(message):
57
+ await message.reply("Hello! I'm a bot.")
58
+
59
+ bot.run()
60
+ ```
61
+
62
+ ## Features
63
+
64
+ - Async-only API (Python 3.8+)
65
+ - Decorator-based handlers: `@bot.command()`, `@bot.hears()`, `@bot.message()`
66
+ - Message object with `.reply()`, `.edit()`, `.delete()` methods
67
+ - Webhook server with HMAC signature verification
68
+ - Automatic retry with exponential backoff
69
+
70
+ ## API Methods
71
+
72
+ | Method | Description |
73
+ | -------------------------------- | --------------------- |
74
+ | `get_me()` | Get bot information |
75
+ | `send_message(chat_id, text)` | Send a message |
76
+ | `edit_message(message_id, text)` | Edit a message |
77
+ | `delete_message(message_id)` | Delete a message |
78
+ | `get_chat(chat_id)` | Get chat info |
79
+ | `get_chat_members(chat_id)` | Get chat members |
80
+ | `leave_chat(chat_id)` | Leave a chat |
81
+ | `set_commands(commands)` | Register bot commands |
82
+ | `set_webhook(url)` | Set webhook URL |
83
+ | `delete_webhook()` | Remove webhook |
84
+
85
+ ## Handlers
86
+
87
+ ### Commands
88
+
89
+ ```python
90
+ @bot.command(['start', 'help'])
91
+ async def start_handler(message):
92
+ await message.reply("Welcome!")
93
+ ```
94
+
95
+ ### Regex
96
+
97
+ ```python
98
+ @bot.hears(r"^/echo (.+)$")
99
+ async def echo_handler(message, match):
100
+ await message.reply(match.group(1))
101
+ ```
102
+
103
+ ### All Messages
104
+
105
+ ```python
106
+ @bot.message
107
+ async def log_message(message):
108
+ print(f"[{message.chat_id}] {message.text}")
109
+ ```
110
+
111
+ ## Webhook
112
+
113
+ ```python
114
+ from xc_bot import Bot, WebhookServer
115
+
116
+ bot = Bot("xbot_token")
117
+
118
+ @bot.command("start")
119
+ async def start(message):
120
+ await message.reply("Hello!")
121
+
122
+ # Set webhook
123
+ await bot.set_webhook("https://your-server.com/webhook", secret="your-secret")
124
+
125
+ # Start webhook server
126
+ server = WebhookServer(bot, port=8443, secret="your-secret")
127
+ server.run()
128
+ ```
129
+
130
+ ## Getting a Token
131
+
132
+ 1. Open chat with @BotConstructor in XaneoConnect
133
+ 2. Send `/newbot`
134
+ 3. Follow the instructions
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,21 @@
1
+ xc_bot/__init__.py,sha256=ZSSt2PXe3PLlHGtJDIP7A2sDnmE2LhmqEg-wYIqJdFU,684
2
+ xc_bot/api.py,sha256=n7U2yO1J_HNEtatSbMw0V8d1j0bwO_tBOTAJofKqNAw,6141
3
+ xc_bot/bot.py,sha256=wp56nehOjbGO7SlaxpmvKgEAhlYn9vFNJ3DV3wHvrWQ,5328
4
+ xc_bot/exceptions.py,sha256=VF4jxgmOkaZfK3t4gGLmR-KNLpZp0FmeKgHZSnAzQik,903
5
+ xc_bot/grpc_client.py,sha256=-soGiJCAfydRqeIph5Cq7o3TwREOhJ7qhIHABTFdW-4,1952
6
+ xc_bot/handlers/__init__.py,sha256=TDZzC9e2gUF-HE6rWtPzw49k3VgA64U3KoSQXMz8leQ,121
7
+ xc_bot/handlers/handler.py,sha256=UCnHqj7zCoODRP302rtu2fwKPm5eHPKthWbsZ--IOlc,2895
8
+ xc_bot/pb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ xc_bot/pb/bot_service_pb2.py,sha256=UUcNanfVAVIqmQwNsZT-MCakmopbFbTauTyhZhmKOe8,2896
10
+ xc_bot/pb/bot_service_pb2_grpc.py,sha256=PqrfJ03HlmBceMUSepnKG2rDH_R6cRoG4xQ5sZ4f8tA,5291
11
+ xc_bot/types/__init__.py,sha256=LBLd1t9fQBpOKpjGokBdZBsp7xfKW2aUvGkIzqsYI3k,258
12
+ xc_bot/types/message.py,sha256=_4Ctfeoazn3a0X9SbGdUUinNaXi9YOiIgrLx1X1BknA,2985
13
+ xc_bot/types/update.py,sha256=dH4NYwBw8Qt6x-h5Uklak8UrWB5Eqbm3im01KFpHWfc,1170
14
+ xc_bot/types/user.py,sha256=B5BOce7wOIO3vSsSJSN64z__Oggyjzn9rKSqI_A92Qg,1110
15
+ xc_bot/webhook/__init__.py,sha256=v2mgclMAvMPJh6VD5AxaRiT6Qwgc6v59uviIPWdnfcU,85
16
+ xc_bot/webhook/server.py,sha256=1jvAyFH3_VNFdDph1pY24UV42wvjfamc4aZvP0ebubE,2829
17
+ xc_bot-1.0.0.dist-info/licenses/LICENSE,sha256=7B4ez0s_T_yYbtXe5iqdQUkU2WYkEvRavLj4QFJMjrA,1067
18
+ xc_bot-1.0.0.dist-info/METADATA,sha256=TPhwkpUssAChuqJrI71eu_dL3lZscgCbdcPz8oSUD6I,3569
19
+ xc_bot-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
+ xc_bot-1.0.0.dist-info/top_level.txt,sha256=x1LO3Yess6VFuhcPEQ4VJcGVlHEnYx4cF2EwI3F-EbY,7
21
+ xc_bot-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Xaneo Team
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
+ xc_bot