maxapi-python 1.2.2__py3-none-any.whl → 1.2.4__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.
- {maxapi_python-1.2.2.dist-info → maxapi_python-1.2.4.dist-info}/METADATA +12 -7
- maxapi_python-1.2.4.dist-info/RECORD +33 -0
- pymax/core.py +55 -37
- pymax/files.py +33 -7
- pymax/interfaces.py +410 -114
- pymax/mixins/auth.py +2 -2
- pymax/mixins/channel.py +3 -5
- pymax/mixins/group.py +33 -14
- pymax/mixins/handler.py +4 -10
- pymax/mixins/message.py +64 -88
- pymax/mixins/scheduler.py +1 -1
- pymax/mixins/self.py +76 -11
- pymax/mixins/socket.py +4 -327
- pymax/mixins/telemetry.py +2 -4
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +5 -354
- pymax/payloads.py +11 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +69 -8
- pymax/static/enum.py +6 -0
- pymax/types.py +82 -28
- pymax/utils.py +90 -0
- maxapi_python-1.2.2.dist-info/RECORD +0 -32
- pymax/mixins/utils.py +0 -27
- {maxapi_python-1.2.2.dist-info → maxapi_python-1.2.4.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.2.dist-info → maxapi_python-1.2.4.dist-info}/licenses/LICENSE +0 -0
pymax/types.py
CHANGED
|
@@ -47,7 +47,7 @@ class Name:
|
|
|
47
47
|
def __init__(
|
|
48
48
|
self,
|
|
49
49
|
name: str | None,
|
|
50
|
-
first_name: None,
|
|
50
|
+
first_name: None | str,
|
|
51
51
|
last_name: str | None,
|
|
52
52
|
type: str | None,
|
|
53
53
|
) -> None:
|
|
@@ -90,16 +90,14 @@ class Names(Name):
|
|
|
90
90
|
def __init__(
|
|
91
91
|
self,
|
|
92
92
|
name: str | None,
|
|
93
|
-
first_name: None,
|
|
93
|
+
first_name: None | str,
|
|
94
94
|
last_name: str | None,
|
|
95
95
|
type: str | None,
|
|
96
96
|
) -> None:
|
|
97
97
|
"""
|
|
98
98
|
Синоним для класса Name.
|
|
99
99
|
"""
|
|
100
|
-
super().__init__(
|
|
101
|
-
name=name, first_name=first_name, last_name=last_name, type=type
|
|
102
|
-
)
|
|
100
|
+
super().__init__(name=name, first_name=first_name, last_name=last_name, type=type)
|
|
103
101
|
|
|
104
102
|
|
|
105
103
|
class Contact:
|
|
@@ -219,7 +217,7 @@ class StickerAttach:
|
|
|
219
217
|
def __init__(
|
|
220
218
|
self,
|
|
221
219
|
author_type: str,
|
|
222
|
-
lottie_url: str,
|
|
220
|
+
lottie_url: str | None,
|
|
223
221
|
url: str,
|
|
224
222
|
sticker_id: int,
|
|
225
223
|
tags: list[str] | None,
|
|
@@ -248,7 +246,7 @@ class StickerAttach:
|
|
|
248
246
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
249
247
|
return cls(
|
|
250
248
|
author_type=data["authorType"],
|
|
251
|
-
lottie_url=data
|
|
249
|
+
lottie_url=data.get("lottieUrl"),
|
|
252
250
|
url=data["url"],
|
|
253
251
|
sticker_id=data["stickerId"],
|
|
254
252
|
tags=data.get("tags"),
|
|
@@ -443,9 +441,7 @@ class VideoAttach:
|
|
|
443
441
|
|
|
444
442
|
|
|
445
443
|
class FileAttach:
|
|
446
|
-
def __init__(
|
|
447
|
-
self, file_id: int, name: str, size: int, token: str, type: AttachType
|
|
448
|
-
) -> None:
|
|
444
|
+
def __init__(self, file_id: int, name: str, size: int, token: str, type: AttachType) -> None:
|
|
449
445
|
self.file_id = file_id
|
|
450
446
|
self.name = name
|
|
451
447
|
self.size = size
|
|
@@ -553,9 +549,7 @@ class Me:
|
|
|
553
549
|
|
|
554
550
|
|
|
555
551
|
class Element:
|
|
556
|
-
def __init__(
|
|
557
|
-
self, type: FormattingType | str, length: int, from_: int | None = None
|
|
558
|
-
) -> None:
|
|
552
|
+
def __init__(self, type: FormattingType | str, length: int, from_: int | None = None) -> None:
|
|
559
553
|
self.type = type
|
|
560
554
|
self.length = length
|
|
561
555
|
self.from_ = from_
|
|
@@ -566,9 +560,7 @@ class Element:
|
|
|
566
560
|
|
|
567
561
|
@override
|
|
568
562
|
def __repr__(self) -> str:
|
|
569
|
-
return (
|
|
570
|
-
f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
|
|
571
|
-
)
|
|
563
|
+
return f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
|
|
572
564
|
|
|
573
565
|
@override
|
|
574
566
|
def __str__(self) -> str:
|
|
@@ -591,7 +583,9 @@ class MessageLink:
|
|
|
591
583
|
|
|
592
584
|
@override
|
|
593
585
|
def __repr__(self) -> str:
|
|
594
|
-
return
|
|
586
|
+
return (
|
|
587
|
+
f"MessageLink(chat_id={self.chat_id!r}, message={self.message!r}, type={self.type!r})"
|
|
588
|
+
)
|
|
595
589
|
|
|
596
590
|
@override
|
|
597
591
|
def __str__(self) -> str:
|
|
@@ -636,6 +630,36 @@ class ReactionInfo:
|
|
|
636
630
|
)
|
|
637
631
|
|
|
638
632
|
|
|
633
|
+
class ContactAttach:
|
|
634
|
+
def __init__(
|
|
635
|
+
self, contact_id: int, first_name: str, last_name: str, name: str, photo_url: str
|
|
636
|
+
) -> None:
|
|
637
|
+
self.contact_id = contact_id
|
|
638
|
+
self.first_name = first_name
|
|
639
|
+
self.last_name = last_name
|
|
640
|
+
self.name = name
|
|
641
|
+
self.photo_url = photo_url
|
|
642
|
+
self.type = AttachType.CONTACT
|
|
643
|
+
|
|
644
|
+
@classmethod
|
|
645
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
646
|
+
return cls(
|
|
647
|
+
contact_id=data["contactId"],
|
|
648
|
+
first_name=data["firstName"],
|
|
649
|
+
last_name=data["lastName"],
|
|
650
|
+
name=data["name"],
|
|
651
|
+
photo_url=data["photoUrl"],
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
@override
|
|
655
|
+
def __repr__(self) -> str:
|
|
656
|
+
return f"ContactAttach(contact_id={self.contact_id!r}, first_name={self.first_name!r}, last_name={self.last_name!r}, name={self.name!r}, photo_url={self.photo_url!r})"
|
|
657
|
+
|
|
658
|
+
@override
|
|
659
|
+
def __str__(self) -> str:
|
|
660
|
+
return f"ContactAttach: {self.name}"
|
|
661
|
+
|
|
662
|
+
|
|
639
663
|
class Message:
|
|
640
664
|
def __init__(
|
|
641
665
|
self,
|
|
@@ -658,6 +682,7 @@ class Message:
|
|
|
658
682
|
| ControlAttach
|
|
659
683
|
| StickerAttach
|
|
660
684
|
| AudioAttach
|
|
685
|
+
| ContactAttach
|
|
661
686
|
]
|
|
662
687
|
| None
|
|
663
688
|
),
|
|
@@ -679,7 +704,13 @@ class Message:
|
|
|
679
704
|
def from_dict(cls, data: dict[Any, Any]) -> Self:
|
|
680
705
|
message = data["message"] if data.get("message") else data
|
|
681
706
|
attaches: list[
|
|
682
|
-
PhotoAttach
|
|
707
|
+
PhotoAttach
|
|
708
|
+
| VideoAttach
|
|
709
|
+
| FileAttach
|
|
710
|
+
| ControlAttach
|
|
711
|
+
| StickerAttach
|
|
712
|
+
| AudioAttach
|
|
713
|
+
| ContactAttach
|
|
683
714
|
] = []
|
|
684
715
|
for a in message.get("attaches", []):
|
|
685
716
|
if a["_type"] == AttachType.PHOTO:
|
|
@@ -694,6 +725,8 @@ class Message:
|
|
|
694
725
|
attaches.append(StickerAttach.from_dict(a))
|
|
695
726
|
elif a["_type"] == AttachType.AUDIO:
|
|
696
727
|
attaches.append(AudioAttach.from_dict(a))
|
|
728
|
+
elif a["_type"] == AttachType.CONTACT:
|
|
729
|
+
attaches.append(ContactAttach.from_dict(a))
|
|
697
730
|
link_value = message.get("link")
|
|
698
731
|
if isinstance(link_value, dict):
|
|
699
732
|
link = MessageLink.from_dict(link_value)
|
|
@@ -778,9 +811,7 @@ class Dialog:
|
|
|
778
811
|
join_time=data["joinTime"],
|
|
779
812
|
created=data["created"],
|
|
780
813
|
last_message=(
|
|
781
|
-
Message.from_dict(data["lastMessage"])
|
|
782
|
-
if data.get("lastMessage")
|
|
783
|
-
else None
|
|
814
|
+
Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
|
|
784
815
|
),
|
|
785
816
|
type=ChatType(data["type"]),
|
|
786
817
|
last_fire_delayed_error_time=data["lastFireDelayedErrorTime"],
|
|
@@ -865,14 +896,10 @@ class Chat:
|
|
|
865
896
|
@classmethod
|
|
866
897
|
def from_dict(cls, data: dict[Any, Any]) -> Self:
|
|
867
898
|
raw_admins = data.get("adminParticipants", {}) or {}
|
|
868
|
-
admin_participants: dict[int, dict[Any, Any]] = {
|
|
869
|
-
int(k): v for k, v in raw_admins.items()
|
|
870
|
-
}
|
|
899
|
+
admin_participants: dict[int, dict[Any, Any]] = {int(k): v for k, v in raw_admins.items()}
|
|
871
900
|
raw_participants = data.get("participants", {}) or {}
|
|
872
901
|
participants: dict[int, int] = {int(k): v for k, v in raw_participants.items()}
|
|
873
|
-
last_msg = (
|
|
874
|
-
Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
|
|
875
|
-
)
|
|
902
|
+
last_msg = Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
|
|
876
903
|
return cls(
|
|
877
904
|
participants_count=data.get("participantsCount", 0),
|
|
878
905
|
access=AccessType(data.get("access", AccessType.PUBLIC.value)),
|
|
@@ -1051,7 +1078,9 @@ class Session:
|
|
|
1051
1078
|
|
|
1052
1079
|
@override
|
|
1053
1080
|
def __str__(self) -> str:
|
|
1054
|
-
return
|
|
1081
|
+
return (
|
|
1082
|
+
f"Session: {self.client} from {self.location} at {self.time} (current={self.current})"
|
|
1083
|
+
)
|
|
1055
1084
|
|
|
1056
1085
|
|
|
1057
1086
|
class Folder:
|
|
@@ -1164,3 +1193,28 @@ class FolderList:
|
|
|
1164
1193
|
@override
|
|
1165
1194
|
def __str__(self) -> str:
|
|
1166
1195
|
return f"FolderList: {len(self.folders)} folders"
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
class ReadState:
|
|
1199
|
+
def __init__(
|
|
1200
|
+
self,
|
|
1201
|
+
unread: int,
|
|
1202
|
+
mark: int,
|
|
1203
|
+
) -> None:
|
|
1204
|
+
self.unread = unread
|
|
1205
|
+
self.mark = mark
|
|
1206
|
+
|
|
1207
|
+
@classmethod
|
|
1208
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
1209
|
+
return cls(
|
|
1210
|
+
unread=data["unread"],
|
|
1211
|
+
mark=data["mark"],
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
@override
|
|
1215
|
+
def __repr__(self) -> str:
|
|
1216
|
+
return f"ReadState(unread={self.unread!r}, mark={self.mark!r})"
|
|
1217
|
+
|
|
1218
|
+
@override
|
|
1219
|
+
def __str__(self) -> str:
|
|
1220
|
+
return f"ReadState: unread={self.unread}, mark={self.mark}"
|
pymax/utils.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import time
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
from typing import Any, NoReturn
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from pymax.exceptions import Error, RateLimitError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MixinsUtils:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def handle_error(data: dict[str, Any]) -> NoReturn:
|
|
14
|
+
error = data.get("payload", {}).get("error")
|
|
15
|
+
localized_message = data.get("payload", {}).get("localizedMessage")
|
|
16
|
+
title = data.get("payload", {}).get("title")
|
|
17
|
+
message = data.get("payload", {}).get("message")
|
|
18
|
+
|
|
19
|
+
if error == "too.many.requests": # TODO: вынести в статик
|
|
20
|
+
raise RateLimitError(
|
|
21
|
+
error=error,
|
|
22
|
+
message=message,
|
|
23
|
+
title=title,
|
|
24
|
+
localized_message=localized_message,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
raise Error(
|
|
28
|
+
error=error,
|
|
29
|
+
message=message,
|
|
30
|
+
title=title,
|
|
31
|
+
localized_message=localized_message,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _fetch_and_extract(url: str, session: requests.Session) -> str | None:
|
|
36
|
+
try:
|
|
37
|
+
js_code = session.get(url, timeout=10).text
|
|
38
|
+
except requests.RequestException:
|
|
39
|
+
return None
|
|
40
|
+
return MixinsUtils._extract_version(js_code)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _extract_version(js_code: str) -> str | None:
|
|
44
|
+
ws_anchor = "wss://ws-api.oneme.ru/websocket"
|
|
45
|
+
pos = js_code.find(ws_anchor)
|
|
46
|
+
if pos == -1:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
snippet = js_code[pos : pos + 2000]
|
|
50
|
+
|
|
51
|
+
match = re.search(r'[:=]\s*"(\d{1,2}\.\d{1,2}\.\d{1,2})"', snippet)
|
|
52
|
+
if match:
|
|
53
|
+
version = match.group(1)
|
|
54
|
+
return version
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def get_current_web_version() -> str | None:
|
|
60
|
+
try:
|
|
61
|
+
html = requests.get("https://web.max.ru/", timeout=10).text
|
|
62
|
+
except requests.RequestException:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
main_chunk_import = html.split("import(")[2].split(")")[0].strip("\"'")
|
|
66
|
+
main_chunk_url = f"https://web.max.ru{main_chunk_import}"
|
|
67
|
+
try:
|
|
68
|
+
main_chunk_code = requests.get(main_chunk_url, timeout=10).text
|
|
69
|
+
except requests.exceptions.RequestException as e:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
arr = main_chunk_code.split("\n")[0].split("[")[1].split("]")[0].split(",")
|
|
73
|
+
urls = []
|
|
74
|
+
for i in arr:
|
|
75
|
+
if "/chunks/" in i:
|
|
76
|
+
url = "https://web.max.ru/_app/immutable" + i[3 : len(i) - 1]
|
|
77
|
+
urls.append(url)
|
|
78
|
+
|
|
79
|
+
session = requests.Session()
|
|
80
|
+
session.headers["User-Agent"] = "Mozilla/5.0"
|
|
81
|
+
if urls:
|
|
82
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
83
|
+
futures = [
|
|
84
|
+
pool.submit(MixinsUtils._fetch_and_extract, url, session) for url in urls
|
|
85
|
+
]
|
|
86
|
+
for f in as_completed(futures):
|
|
87
|
+
ver = f.result()
|
|
88
|
+
if ver:
|
|
89
|
+
return ver
|
|
90
|
+
return None
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
-
pymax/core.py,sha256=OXGNaQ0pDaf6Ofr1Fb9m7vh5ffpbiMyvUMM0EfwlnIQ,14907
|
|
3
|
-
pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
|
|
4
|
-
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
-
pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
|
|
6
|
-
pymax/filters.py,sha256=gSHPJ1Vi37HKPxf0jRRv9Q3iGwhiQjw1MGrCaouqHzs,4325
|
|
7
|
-
pymax/formatter.py,sha256=RJ_5VbY7Li8UM3xL1AvcXo8v1iYnY8GvDDkreaFqtnY,860
|
|
8
|
-
pymax/formatting.py,sha256=XRtuXJGweuNZevJFdPxksDftIrfuMGEA-AOUc_v6IhQ,2484
|
|
9
|
-
pymax/interfaces.py,sha256=wKF1z1QRw8LcjvM9rzSHWXTK6gPb6sDt2UGiQLvyMf8,8790
|
|
10
|
-
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
-
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
-
pymax/payloads.py,sha256=GuTLK6HYe_bLW3ardgpKeZ98f79j349tD_6B6EwkGww,7879
|
|
13
|
-
pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
|
|
14
|
-
pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
|
|
15
|
-
pymax/mixins/auth.py,sha256=e90vIpEOwAjUxgYMYaG7R6jR_5t9rKsei_mTBQUirL4,14716
|
|
16
|
-
pymax/mixins/channel.py,sha256=W52YnBay1sUYXxF9oAWsz44ZUh_s45jSvKmAyjTbULM,5357
|
|
17
|
-
pymax/mixins/group.py,sha256=LqI1QHmZlmtuQ0-4H1MrNeBV-O9SMDMfHT9f4B_2poE,15189
|
|
18
|
-
pymax/mixins/handler.py,sha256=ETnI8fA386LYJGjWtUhhWzQHREUA78di1aO1oWwtscA,12523
|
|
19
|
-
pymax/mixins/message.py,sha256=AznKKmTMxdzsYl8IecT43RjWpGvlQM85GzSNGFbI8BA,33279
|
|
20
|
-
pymax/mixins/scheduler.py,sha256=rcMfgfZnzu5V6MkcCg6uRgbi-jkc7UyqOjemulydWbc,964
|
|
21
|
-
pymax/mixins/self.py,sha256=Be5L64eNYylGM-NmoxFpQZv1ohsC1Dx_Cs3Om__V96s,6976
|
|
22
|
-
pymax/mixins/socket.py,sha256=tdHgd1NwWoEZhHCDd74XLOHFKUq-rladxhXV8Z_-APU,22860
|
|
23
|
-
pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
|
|
24
|
-
pymax/mixins/user.py,sha256=RSZd4t-aq8P2k3cVzNVWBkUf-_xTWILrBzwxLRgk1pw,9450
|
|
25
|
-
pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
|
|
26
|
-
pymax/mixins/websocket.py,sha256=GpdboEVWzyN1qLTcsgKZym6TlPnklcQuNeXJ5YKwg8c,17724
|
|
27
|
-
pymax/static/constant.py,sha256=nM0svv3VpsVxK-RqoADn9qsTdQvB-IYv0Sgv-bQcWs4,1116
|
|
28
|
-
pymax/static/enum.py,sha256=Hk0e6zSbGOJC_9Aw7gNXX3hcavnjzQfDyr8vjW22cFo,4648
|
|
29
|
-
maxapi_python-1.2.2.dist-info/METADATA,sha256=rgiQKdSqYAO743n6jWOy0F76jZyjaGMY7A6qUlHlk64,6753
|
|
30
|
-
maxapi_python-1.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
31
|
-
maxapi_python-1.2.2.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
32
|
-
maxapi_python-1.2.2.dist-info/RECORD,,
|
pymax/mixins/utils.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from typing import Any, NoReturn
|
|
2
|
-
|
|
3
|
-
from pymax.exceptions import Error, RateLimitError
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class MixinsUtils:
|
|
7
|
-
@staticmethod
|
|
8
|
-
def handle_error(data: dict[str, Any]) -> NoReturn:
|
|
9
|
-
error = data.get("payload", {}).get("error")
|
|
10
|
-
localized_message = data.get("payload", {}).get("localizedMessage")
|
|
11
|
-
title = data.get("payload", {}).get("title")
|
|
12
|
-
message = data.get("payload", {}).get("message")
|
|
13
|
-
|
|
14
|
-
if error == "too.many.requests": # TODO: вынести в статик
|
|
15
|
-
raise RateLimitError(
|
|
16
|
-
error=error,
|
|
17
|
-
message=message,
|
|
18
|
-
title=title,
|
|
19
|
-
localized_message=localized_message,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
raise Error(
|
|
23
|
-
error=error,
|
|
24
|
-
message=message,
|
|
25
|
-
title=title,
|
|
26
|
-
localized_message=localized_message,
|
|
27
|
-
)
|
|
File without changes
|
|
File without changes
|