autowebx 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.
autowebx/files.py ADDED
@@ -0,0 +1,82 @@
1
+ from threading import Lock, Thread, Condition
2
+
3
+ __lock = Lock()
4
+
5
+
6
+ def add(text: str, to: str) -> None:
7
+ __lock.acquire()
8
+ open(to, 'a').write(text)
9
+ __lock.release()
10
+
11
+
12
+ def load(*paths: str) -> list[str] | list[list[str]]:
13
+ lists = []
14
+ for file_name in paths:
15
+ try:
16
+ lists.append(open(file_name, 'r').read().strip().split('\n'))
17
+ except FileNotFoundError:
18
+ open(file_name, 'w').write('')
19
+ lists.append([])
20
+ return lists if len(lists) > 1 else lists[0]
21
+
22
+
23
+ def replace(old: str, new: str, path: str) -> None:
24
+ with __lock:
25
+ old_content = open(path, 'r').read()
26
+ new_content = old_content.replace(old, new)
27
+ with __lock:
28
+ open(path, 'w').write(new_content)
29
+
30
+
31
+ class File:
32
+ def __init__(self, path: str) -> None:
33
+ self.__path = path
34
+ with open(self.__path, 'r') as file:
35
+ self.__buffer = file.read()
36
+ self.__changed = False
37
+ self.__closed = False
38
+ self.__lock = Lock()
39
+ self.__condition = Condition(self.__lock)
40
+ self.lines = self.__buffer.split('\n')
41
+ self.__thread = Thread(target=self.__update, daemon=True)
42
+ self.__thread.start()
43
+
44
+ def __update(self):
45
+ while True:
46
+ with self.__condition:
47
+ while not self.__changed and not self.__closed:
48
+ self.__condition.wait()
49
+ if self.__closed:
50
+ break
51
+ if not self.__buffer.strip(): # Avoid writing empty content
52
+ continue
53
+ with open(self.__path, 'w') as file:
54
+ file.write(self.__buffer)
55
+ self.__changed = False
56
+
57
+ def replace(self, old: str, new: str) -> None:
58
+ with self.__condition:
59
+ self.__buffer = self.__buffer.replace(old, new)
60
+ self.lines = self.__buffer.split('\n')
61
+ self.__changed = True
62
+ self.__condition.notify()
63
+
64
+ def add(self, text: str) -> None:
65
+ with self.__condition:
66
+ self.__buffer += text
67
+ self.lines = self.__buffer.split('\n')
68
+ self.__changed = True
69
+ self.__condition.notify()
70
+
71
+ def close(self) -> None:
72
+ with self.__condition:
73
+ if not self.__closed:
74
+ with open(self.__path, 'w') as file:
75
+ file.write(self.__buffer)
76
+ self.__closed = True
77
+ self.__condition.notify()
78
+ self.__thread.join()
79
+
80
+ def __del__(self):
81
+ if not self.__closed:
82
+ self.close()
autowebx/five_sim.py ADDED
@@ -0,0 +1,56 @@
1
+ import time
2
+ import requests
3
+
4
+ DOMAIN = "5sim.net"
5
+
6
+
7
+ class Phone:
8
+ def __init__(self, number_id, phone):
9
+ self.id = number_id
10
+ self.number = phone
11
+
12
+
13
+ class FiveSim:
14
+ def __init__(self, token):
15
+ self.headers = {
16
+ 'Authorization': 'Bearer ' + token,
17
+ 'Accept': 'application/json',
18
+ }
19
+
20
+ def balance(self):
21
+ response = requests.get(f'https://{DOMAIN}/v1/user/profile', headers=self.headers)
22
+ return response.json()['balance']
23
+
24
+ def buy_activation_number(self, country: str, operator: str, product: str):
25
+ url = f'https://{DOMAIN}/v1/user/buy/activation/{country}/{operator}/{product}'
26
+ response = requests.get(url, headers=self.headers).json()
27
+ return Phone(response['id'], response['phone'][1:])
28
+
29
+ def get_codes(self, phone: Phone, timeout: float = 30):
30
+ start = time.time()
31
+ while True:
32
+ try:
33
+ response = requests.get(f'https://{DOMAIN}/v1/user/check/{phone.id}', headers=self.headers).json()
34
+ return [sms['code'] for sms in response['sms']]
35
+ except (ValueError, IndexError):
36
+ pass
37
+
38
+ if time.time() - start > timeout:
39
+ raise TimeoutError
40
+
41
+
42
+ def min_cost_providers(country: str, product: str):
43
+ response = requests.get(f'https://{DOMAIN}/v1/guest/prices?country={country}&product={product}').json()
44
+ operators0 = []
45
+ for operator in response[country][product]:
46
+ if response[country][product][operator]['count'] != 0:
47
+ operators0.append(operator)
48
+ min_cost = response[country][product][operators0[0]]['cost']
49
+ for operator in operators0:
50
+ if response[country][product][operator]['cost'] < min_cost:
51
+ min_cost = response[country][product][operator]['cost']
52
+ operators1 = []
53
+ for operator in operators0:
54
+ if response[country][product][operator]['cost'] == min_cost:
55
+ operators1.append(operator)
56
+ return operators1
autowebx/inboxes.py ADDED
@@ -0,0 +1,90 @@
1
+ from time import time, sleep
2
+
3
+ from requests import Session
4
+
5
+
6
+ class Message:
7
+ def __init__(self, uid, sender, subject, received, receiver):
8
+ self.id = uid
9
+ self.sender = sender
10
+ self.subject = subject
11
+ self.received = received
12
+ self.receiver = receiver
13
+
14
+
15
+ class Inboxes:
16
+ def __init__(self, email, timeout: int = 30):
17
+ url = "https://inboxes.com/api/v2/domain"
18
+
19
+ headers = {
20
+ 'Host': 'inboxes.com',
21
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0',
22
+ 'Accept': '*/*',
23
+ 'Accept-Language': 'en-US,en;q=0.5',
24
+ 'Accept-Encoding': 'gzip, deflate',
25
+ 'Referer': 'https://inboxes.com/',
26
+ 'DNT': '1',
27
+ 'Sec-GPC': '1',
28
+ 'Connection': 'close',
29
+ 'Sec-Fetch-Dest': 'empty',
30
+ 'Sec-Fetch-Mode': 'cors',
31
+ 'Sec-Fetch-Site': 'same-origin',
32
+ 'Priority': 'u=0'
33
+ }
34
+ self.session = Session()
35
+ self.session.get(url, headers=headers)
36
+ self.timeout = timeout
37
+ self.messages = list()
38
+ self.email = email
39
+
40
+ def inbox(self, timeout=None) -> list[Message]: # 102
41
+ url = f"https://inboxes.com/api/v2/inbox/{self.email}"
42
+
43
+ headers = {
44
+ 'Host': 'inboxes.com',
45
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0',
46
+ 'Accept': '*/*',
47
+ 'Accept-Language': 'en-US,en;q=0.5',
48
+ 'Accept-Encoding': 'gzip, deflate',
49
+ 'Referer': 'https://inboxes.com/',
50
+ 'authorization': 'Bearer null',
51
+ 'DNT': '1',
52
+ 'Sec-GPC': '1',
53
+ 'Connection': 'close',
54
+ 'Sec-Fetch-Dest': 'empty',
55
+ 'Sec-Fetch-Mode': 'cors',
56
+ 'Sec-Fetch-Site': 'same-origin',
57
+ 'Priority': 'u=0'
58
+ }
59
+ start = time()
60
+ while time() - start < (timeout or self.timeout):
61
+ response = self.session.get(url, headers=headers).json()
62
+ if result := [Message(msg['uid'], msg['f'], msg['s'], msg['r'], self.email) for msg in
63
+ response.get('msgs', [])]:
64
+ for msg in result:
65
+ self.messages.append(msg)
66
+ return self.messages
67
+ sleep(0.5)
68
+ return []
69
+
70
+ def html(self, msg: Message): # 2746
71
+ url = f"https://inboxes.com/api/v2/message/{msg.id}"
72
+
73
+ headers = {
74
+ 'Host': 'inboxes.com',
75
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0',
76
+ 'Accept': '*/*',
77
+ 'Accept-Language': 'en-US,en;q=0.5',
78
+ 'Accept-Encoding': 'gzip, deflate',
79
+ 'Referer': 'https://inboxes.com/',
80
+ 'Content-Type': 'application/json',
81
+ 'DNT': '1',
82
+ 'Sec-GPC': '1',
83
+ 'Connection': 'close',
84
+ 'Sec-Fetch-Dest': 'empty',
85
+ 'Sec-Fetch-Mode': 'cors',
86
+ 'Sec-Fetch-Site': 'same-origin',
87
+ 'Priority': 'u=4'
88
+ }
89
+
90
+ return self.session.get(url, headers=headers).json()['html']
autowebx/mail_tm.py ADDED
@@ -0,0 +1,69 @@
1
+ import time
2
+
3
+ import requests
4
+ from requests import JSONDecodeError
5
+
6
+ from autoweb import AccountError
7
+ from autoweb.account import Account
8
+
9
+
10
+ def domains():
11
+ return [element['domain'] for element in requests.get('https://api.mail.tm/domains').json()['hydra:member']]
12
+
13
+
14
+ class MailTMAccount(Account):
15
+
16
+ def __init__(
17
+ self, **kwargs
18
+ ):
19
+ super().__init__(**kwargs)
20
+ data = {
21
+ 'address': self.email.lower(),
22
+ 'password': self.password
23
+ }
24
+ while True:
25
+ try:
26
+ response = requests.post('https://api.mail.tm/accounts', json=data).json()
27
+ self.id = response['id']
28
+ break
29
+ except KeyError:
30
+ raise AccountError()
31
+ except JSONDecodeError:
32
+ pass
33
+ while True:
34
+ try:
35
+ token = requests.post('https://api.mail.tm/token', json=data).json()['token']
36
+ break
37
+ except JSONDecodeError:
38
+ pass
39
+ self.headers = {'Authorization': f"Bearer {token}"}
40
+
41
+ def messages(self, timeout: float = 30):
42
+ start = time.time()
43
+ while True:
44
+ while True:
45
+ try:
46
+ response = requests.get('https://api.mail.tm/messages', headers=self.headers).json()
47
+ break
48
+ except JSONDecodeError:
49
+ pass
50
+ if time.time() - start > timeout:
51
+ raise TimeoutError
52
+ if response['hydra:totalItems'] > 0:
53
+ messages = []
54
+ for member in response['hydra:member']:
55
+ url = f'https://api.mail.tm/messages/{member["id"]}'
56
+ while True:
57
+ try:
58
+ messages.append(requests.get(url, headers=self.headers).json()['html'])
59
+ break
60
+ except JSONDecodeError:
61
+ pass
62
+
63
+ print(messages)
64
+ if response['hydra:totalItems'] == 1:
65
+ return messages[0][0]
66
+ else:
67
+ return messages[0]
68
+ if time.time() - start > timeout:
69
+ raise TimeoutError('No message received')
autowebx/mediafire.py ADDED
@@ -0,0 +1,18 @@
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+
4
+
5
+ class File:
6
+ def __init__(self, file_id):
7
+ self.normal_download_link = f'https://www.mediafire.com/file/{file_id}/'
8
+
9
+ def __response__(self):
10
+ soup = BeautifulSoup(requests.get(self.normal_download_link).text, 'html.parser')
11
+ url = soup.find('a', attrs={'aria-label': "Download file"}).get('href')
12
+ return requests.get(url)
13
+
14
+ def content(self):
15
+ return self.__response__().content
16
+
17
+ def text(self):
18
+ return self.__response__().text
autowebx/panels.py ADDED
@@ -0,0 +1,161 @@
1
+ from base64 import b64encode
2
+ from datetime import datetime, timezone, timedelta
3
+ from multiprocessing.connection import Client
4
+ from multiprocessing.connection import Listener
5
+ from threading import Thread, Lock
6
+ from time import time
7
+
8
+ import requests
9
+ from requests import get
10
+
11
+
12
+ class Premiumy:
13
+ __url = 'https://api.premiumy.net/v1.0/json'
14
+
15
+ def __init__(self, api_key: str, port: int = 3010):
16
+ self.__headers = {
17
+ "Content-type": "application/json",
18
+ "Api-Key": api_key
19
+ }
20
+
21
+ self.__data = {
22
+ "id": None,
23
+ "jsonrpc": "2.0",
24
+ "method": "sms.mdr_full:get_list",
25
+ "params": {
26
+ "filter": {
27
+ "start_date": datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S'),
28
+ },
29
+ }
30
+ }
31
+
32
+ self.report = dict()
33
+ Thread(None, self.__report).start()
34
+
35
+ while True:
36
+ with Listener(('localhost', port)) as listener:
37
+ with listener.accept() as conn:
38
+ conn.send(self.report)
39
+
40
+ def __report(self):
41
+ while True:
42
+ # noinspection PyBroadException
43
+ try:
44
+ response = requests.post(url=self.__url, json=self.__data, headers=self.__headers).json()
45
+ self.report.update({row['phone']: row['message'] for row in response['result']['mdr_full_list']})
46
+ except Exception:
47
+ pass
48
+
49
+
50
+ class Sniper:
51
+ __url = "http://51.38.64.110/ints/agent/res/data_smscdr.php"
52
+
53
+ def __init__(self, phpsessid, port: int = 3010):
54
+ self.__headers = {
55
+ 'X-Requested-With': 'XMLHttpRequest',
56
+ 'Cookie': f'PHPSESSID={phpsessid}'
57
+ }
58
+
59
+ self.report = dict()
60
+ Thread(None, self.__report).start()
61
+
62
+ while True:
63
+ with Listener(('localhost', port)) as listener:
64
+ with listener.accept() as conn:
65
+ conn.send(self.report)
66
+
67
+ def __report(self):
68
+ while True:
69
+ # noinspection PyBroadException
70
+ try:
71
+ parameters = {
72
+ 'fdate1': (datetime.now(timezone.utc) - timedelta(minutes=3)).strftime("%Y-%m-%d %H:%M:%S"),
73
+ 'fdate2': (datetime.now(timezone.utc) + timedelta(minutes=3)).strftime("%Y-%m-%d %H:%M:%S"),
74
+ }
75
+ response = requests.post(url=self.__url, params=parameters, headers=self.__headers).json()
76
+ self.report.update({row[2]: row[5] for row in response['aaData'][:-1]})
77
+ except Exception:
78
+ pass
79
+
80
+
81
+ class PSCall:
82
+ __headers = {
83
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
84
+ 'application/signed-exchange;v=b3;q=0.7',
85
+ 'Accept-Language': 'en-GB,en;q=0.9',
86
+ 'Cache-Control': 'max-age=0',
87
+ 'Connection': 'keep-alive',
88
+ 'Upgrade-Insecure-Requests': '1',
89
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
90
+ 'Chrome/131.0.0.0 Safari/537.36',
91
+ }
92
+
93
+ def __init__(self, key: str, port: int = 3010):
94
+ # noinspection HttpUrlsUsage
95
+ self.__url = f'http://pscall.net/restapi/smsreport?key={key}'
96
+ self.report = dict()
97
+ Thread(None, self.__report).start()
98
+ while True:
99
+ with Listener(('localhost', port)) as listener:
100
+ with listener.accept() as conn:
101
+ conn.send(self.report)
102
+
103
+ def __report(self):
104
+ while True:
105
+ # noinspection PyBroadException
106
+ try:
107
+ response = get(self.__url, headers=PSCall.__headers, verify=False).json()
108
+ self.report.update({row['num']: row['sms'] for row in response['data']})
109
+ except Exception as _:
110
+ pass
111
+
112
+
113
+ class Ziva:
114
+ def __init__(self, credentials, port: int = 3010):
115
+ self.__page_count = 0
116
+ self.__id = 0
117
+ auth = b64encode(credentials.encode('utf-8')).decode('utf-8')
118
+ self.__headers = {'Authorization': f'Basic {auth}'}
119
+ self.__url = 'http://zivastats.com/rest/sms?per-page=1000'
120
+ self.report = dict()
121
+ Thread(None, self.__report).start()
122
+ while True:
123
+ with Listener(('localhost', port)) as listener:
124
+ with listener.accept() as conn:
125
+ conn.send(self.report)
126
+
127
+ def __report(self):
128
+ while True:
129
+ # noinspection PyBroadException
130
+ try:
131
+ response = get(self.__url, headers=self.__headers, verify=False)
132
+ response = response.json()
133
+ self.report.update({row['destination_addr']: row['short_message'] for row in response})
134
+ except Exception as _:
135
+ pass
136
+
137
+
138
+ class ReportNotRunningError(Exception):
139
+ pass
140
+
141
+
142
+ class ReportReader:
143
+ def __init__(self, port: int = 3010, timeout: int = 30):
144
+ self.port = port
145
+ self.__lock = Lock()
146
+ self.timeout = timeout
147
+
148
+ def message(self, number):
149
+ start = time()
150
+ while time() - start < self.timeout:
151
+ try:
152
+ with self.__lock:
153
+ try:
154
+ with Client(('localhost', 3010)) as conn:
155
+ if result := conn.recv().get(number, None):
156
+ return result
157
+ except ConnectionRefusedError:
158
+ raise ReportNotRunningError("Run the report script to receive messages from panels")
159
+ except ConnectionResetError:
160
+ pass
161
+ return None
autowebx/proxy.py ADDED
@@ -0,0 +1,51 @@
1
+ import random
2
+
3
+
4
+ # noinspection HttpUrlsUsage
5
+ class Proxy:
6
+ def __init__(
7
+ self, text: str
8
+ ):
9
+ info = text.split('@')
10
+ if len(info) == 2:
11
+ authentication_information, self.server = info
12
+ self.username, self.password = authentication_information.split(':')
13
+ else:
14
+ info = text.split(':')
15
+ if len(info) == 2:
16
+ self.username = self.password = None
17
+ self.server = text
18
+ elif len(info) == 4:
19
+ self.username = info[2]
20
+ self.password = info[3]
21
+ self.server = f'{info[0]}:{info[1]}'
22
+ else:
23
+ raise TypeError(f"""Proxy must have one of the following formats:
24
+ username:password@proxyserver:port
25
+ proxy-server:port:username:password
26
+ proxy-server:port
27
+ """)
28
+
29
+ def for_requests(self):
30
+ if self.username and self.password:
31
+ server = f'http://{self.username}:{self.password}@{self.server}'
32
+ else:
33
+ server = f'http://{self.server}'
34
+ return {"http": server, 'https': server}
35
+
36
+ def for_playwright(self):
37
+ return {
38
+ 'server': f'http://{self.server}',
39
+ 'username': self.username,
40
+ 'password': self.password,
41
+ } if self.username and self.password else {'server': f'http://{self.server}'}
42
+
43
+
44
+ class LunaProxy(Proxy):
45
+ __host_prefixes = ['pr', 'as', 'eu', 'na']
46
+
47
+ def __init__(self, username: str, password: str, region: str, session_time: int = 3):
48
+ super().__init__('0:0')
49
+ self.username = f'user-{username}-region-{region}-sesstime-{session_time}'
50
+ self.password = password
51
+ self.server = f'{random.choice(LunaProxy.__host_prefixes)}.6n3arhxy.lunaproxy.net:12233'
autowebx/remotix.py ADDED
@@ -0,0 +1,89 @@
1
+ import json
2
+ import os
3
+ import random
4
+ from socket import gethostname
5
+ from threading import Thread
6
+ from time import sleep
7
+ from typing import Optional, Any
8
+
9
+ from requests import post
10
+
11
+ from autoweb.auto_save_dict import AutoSaveDict
12
+
13
+ BASE_URL = 'http://localhost:8000'
14
+
15
+
16
+ class InvalidKey(Exception):
17
+ pass
18
+
19
+ class Run:
20
+ def __init__(self, key: str, stats: Optional[dict[str, Any]] = None, *files: str):
21
+ base_dir = os.path.join(os.getenv("APPDATA"), 'Remotix')
22
+ if not os.path.exists(base_dir):
23
+ os.makedirs(base_dir)
24
+
25
+ file_path = os.path.join(base_dir, "data.json")
26
+ asd = AutoSaveDict(file_path)
27
+ if devic_id := asd.get('device_id', None):
28
+ url = f'{BASE_URL}/api/verify/?api_key={key}&device_id={devic_id}'
29
+ if not (response := post(url).json())['valid']:
30
+ raise InvalidKey(response['message'])
31
+ else:
32
+ device_name = gethostname()
33
+ url = f'{BASE_URL}/api/verify/?api_key={key}&device_name={device_name}'
34
+ asd['device_id'] = (response := post(url).json())['device_id']
35
+ if not response['valid']:
36
+ raise InvalidKey(response['message'])
37
+
38
+ url = f'{BASE_URL}/usage/'
39
+ data = {
40
+ 'api_key': key,
41
+ 'device_id': asd['device_id'],
42
+ 'stats': json.dumps(stats),
43
+ }
44
+ self.__run_id = post(
45
+ url,
46
+ data=data,
47
+ files=[("files", (file.split('/')[-1], open(file, "rb"), "text/plain")) for file in
48
+ files] if files else None
49
+ ).json()['run_id']
50
+
51
+ self.__key = key
52
+ self.__stats = dict()
53
+ self.__done = False
54
+
55
+ Thread(None, self.__log, 'log').start()
56
+
57
+ def __log(self):
58
+ while not self.__done:
59
+ # noinspection PyBroadException
60
+ try:
61
+ url = f'{BASE_URL}/usage/'
62
+ data = {
63
+ 'api_key': self.__key,
64
+ 'run_id': self.__run_id,
65
+ 'stats': json.dumps(self.__stats)
66
+ }
67
+ post(url, data=data)
68
+ except Exception:
69
+ pass
70
+
71
+ def log(self, key: str) -> None:
72
+ self.__stats[key] = self.__stats.get(key, 0) + 1
73
+
74
+ def done(self):
75
+ self.__done = True
76
+
77
+
78
+ if __name__ == '__main__':
79
+ run = Run(
80
+ 'bcab38ce-cd3a-4948-83f0-f577513a21e7',
81
+ {'threads': 3, 'total': 10}
82
+ )
83
+
84
+ for _ in range(1000):
85
+ run.log(random.choices(['Success', 'Fail'], weights=[90, 10], k=1)[0])
86
+ sleep(random.uniform(0, 1))
87
+
88
+ run.done()
89
+
autowebx/temp_mail.py ADDED
@@ -0,0 +1,50 @@
1
+ import random
2
+
3
+ from multipledispatch import dispatch
4
+ from requests import get, post
5
+
6
+ from autoweb.account import generate_username
7
+
8
+ domains = [domain['name'] for domain in get("https://api.internal.temp-mail.io/api/v4/domains").json()['domains']]
9
+
10
+
11
+ class Email:
12
+ @dispatch(str, str)
13
+ def __init__(self, name: str = generate_username(), domain: str = random.choice(domains)):
14
+ payload = {"name": name, "domain": domain}
15
+ response = post('https://api.internal.temp-mail.io/api/v3/email/new', json=payload).json()
16
+ self.address = response['email']
17
+ self.token = response['token']
18
+
19
+ @dispatch(int, int)
20
+ def __init__(self, min_name_length: int = 10, max_name_length: int = 10):
21
+ payload = {"min_name_length": min_name_length, "max_name_length": max_name_length}
22
+ response = post('https://api.internal.temp-mail.io/api/v3/email/new', json=payload).json()
23
+ self.address = response['email']
24
+ self.token = response['token']
25
+
26
+ @dispatch()
27
+ def __init__(self):
28
+ self.__init__(10, 10)
29
+
30
+ def get_messages(self):
31
+ return get_messages(self.address)
32
+
33
+
34
+ def get_messages(email: str):
35
+ response = get(f'https://api.internal.temp-mail.io/api/v3/email/{email}/messages').json()
36
+ messages = [Message(message['id'], message['body_text'], message['body_html']) for message in response]
37
+ return messages[0] if len(messages) == 1 else messages
38
+
39
+
40
+ class Message:
41
+ def __init__(self, id_: str, body_text: str, body_html: str):
42
+ self.id = id_
43
+ self.body_text = body_text
44
+ self.body_html = body_html
45
+
46
+ def __hash__(self):
47
+ return hash(self.id)
48
+
49
+ def __eq__(self, other):
50
+ return other is Message and self.id == other.id