interpals-api 2.3.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.
- interpals_api/__init__.py +161 -0
- interpals_api/api.py +655 -0
- interpals_api/cookie.py +48 -0
- interpals_api/errors.py +65 -0
- interpals_api/parsers/__init__.py +3 -0
- interpals_api/parsers/chat_parser.py +132 -0
- interpals_api/parsers/friends_parser.py +53 -0
- interpals_api/parsers/pictures_parser.py +91 -0
- interpals_api/parsers/profile_parser.py +124 -0
- interpals_api/session.py +243 -0
- interpals_api/utils.py +23 -0
- interpals_api-2.3.0.dist-info/METADATA +196 -0
- interpals_api-2.3.0.dist-info/RECORD +15 -0
- interpals_api-2.3.0.dist-info/WHEEL +5 -0
- interpals_api-2.3.0.dist-info/top_level.txt +1 -0
interpals_api/api.py
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Classes that implement sync and async API calls and parsing. To create an
|
|
3
|
+
instance session object is required.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import asyncio
|
|
8
|
+
from urllib.parse import urlencode, urlparse, parse_qs
|
|
9
|
+
from time import sleep
|
|
10
|
+
from typing import List, Dict, Any, Generator, Optional
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
import aiohttp
|
|
14
|
+
from bs4 import BeautifulSoup
|
|
15
|
+
|
|
16
|
+
from .errors import *
|
|
17
|
+
from .cookie import Cookie
|
|
18
|
+
from .utils import find_csrf_token
|
|
19
|
+
from .parsers.profile_parser import ProfileParser
|
|
20
|
+
from .parsers.chat_parser import ChatParser
|
|
21
|
+
from .parsers.friends_parser import FriendsParser
|
|
22
|
+
from .parsers.pictures_parser import PicturesParser
|
|
23
|
+
from .session import Session, SessionAsync
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Api:
|
|
27
|
+
"""
|
|
28
|
+
Class for synchronous API.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
timeout = 3.0
|
|
32
|
+
"""
|
|
33
|
+
Timeout for HTTP calls. Override it if needed.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
host = "interpals.net"
|
|
37
|
+
"""
|
|
38
|
+
Host address.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
|
|
42
|
+
"(KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"
|
|
43
|
+
"""
|
|
44
|
+
`User-Agent` header in each HTTP call. Override it if needed.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, session: Session):
|
|
48
|
+
"""
|
|
49
|
+
Create an API instance.
|
|
50
|
+
"""
|
|
51
|
+
self._session = session
|
|
52
|
+
|
|
53
|
+
def check_auth(self) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Check whether the session is authorized.
|
|
56
|
+
"""
|
|
57
|
+
if self._session:
|
|
58
|
+
response = self._get(self._session.username)
|
|
59
|
+
return self._check_body_for_auth(response.text)
|
|
60
|
+
else:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def view(self, user: str):
|
|
64
|
+
"""
|
|
65
|
+
View the given user.
|
|
66
|
+
"""
|
|
67
|
+
self._get(user, check_auth=True)
|
|
68
|
+
|
|
69
|
+
def profile(self, user: str) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Get the profile information of the user.
|
|
72
|
+
"""
|
|
73
|
+
response = self._get(user, check_auth=True)
|
|
74
|
+
|
|
75
|
+
if "User not found." in response.text:
|
|
76
|
+
raise APIError("user not found")
|
|
77
|
+
|
|
78
|
+
parser = ProfileParser()
|
|
79
|
+
data = parser.parse(response.text)
|
|
80
|
+
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
def visitors(self) -> List[str]:
|
|
84
|
+
"""
|
|
85
|
+
Get users who viewed your profile.
|
|
86
|
+
"""
|
|
87
|
+
response = self._get("/app/views", check_auth=True)
|
|
88
|
+
soup = BeautifulSoup(response.text, "lxml")
|
|
89
|
+
items = soup.find_all('div', class_='vBottomTxt')
|
|
90
|
+
users = []
|
|
91
|
+
for item in items:
|
|
92
|
+
user = item.find('a')['href'][1:].split('?')[0]
|
|
93
|
+
users.append(user)
|
|
94
|
+
return users
|
|
95
|
+
|
|
96
|
+
def search(self, options: dict, limit: int = 1000,
|
|
97
|
+
timeout: float = 0.0) -> Generator[str, None, None]:
|
|
98
|
+
"""
|
|
99
|
+
Search for users according to the search `options`. The total number
|
|
100
|
+
of users is `limit`. Since users are requested as pages, `timeout`
|
|
101
|
+
is a timeout between page loading.
|
|
102
|
+
"""
|
|
103
|
+
response = self._get("/app/search", check_auth=True)
|
|
104
|
+
csrf_token = find_csrf_token(response.text)
|
|
105
|
+
|
|
106
|
+
offset = 0
|
|
107
|
+
params = self._prepare_search_params(options)
|
|
108
|
+
params['csrf_token'] = csrf_token
|
|
109
|
+
while True:
|
|
110
|
+
params['offset'] = str(offset)
|
|
111
|
+
|
|
112
|
+
response = self._get("/app/search", params, check_auth=True)
|
|
113
|
+
users = self._parse_search_result(response.text)
|
|
114
|
+
|
|
115
|
+
if not users:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
for user in users:
|
|
119
|
+
yield user
|
|
120
|
+
offset += 1
|
|
121
|
+
if offset >= limit:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
sleep(timeout)
|
|
125
|
+
|
|
126
|
+
def get_uid(self, user: str) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Get unique user identifier `uid`.
|
|
129
|
+
"""
|
|
130
|
+
profile_info = self.profile(user)
|
|
131
|
+
return profile_info['uid']
|
|
132
|
+
|
|
133
|
+
def get_thread_id(self, uid: str) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Get a chat thread_id with a user given as `uid`.
|
|
136
|
+
"""
|
|
137
|
+
params = {'action': 'send', 'uid': uid}
|
|
138
|
+
response = self._get("/pm.php", params)
|
|
139
|
+
assert response.status_code == 301
|
|
140
|
+
|
|
141
|
+
location = response.headers['Location']
|
|
142
|
+
thread_id = parse_qs(urlparse(location).query)['thread_id'][0]
|
|
143
|
+
|
|
144
|
+
return thread_id
|
|
145
|
+
|
|
146
|
+
def chat(self, count: int = 9, offset: int = 0) -> Dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Get general chat information in count `count` with the offset `offset`.
|
|
149
|
+
"""
|
|
150
|
+
chat_parser = ChatParser()
|
|
151
|
+
|
|
152
|
+
response = self._get("/pm.php", check_auth=True)
|
|
153
|
+
maxmsgid = chat_parser.parse_maxmsgid(response.text)
|
|
154
|
+
unread = chat_parser.parse_unread(response.text)
|
|
155
|
+
|
|
156
|
+
chats = []
|
|
157
|
+
while len(chats) < count:
|
|
158
|
+
params = {
|
|
159
|
+
'action': 'more_threads',
|
|
160
|
+
'from': str(offset),
|
|
161
|
+
'filter': 'all',
|
|
162
|
+
'max_msg_id': maxmsgid,
|
|
163
|
+
}
|
|
164
|
+
response = self._post("/pm.php", params)
|
|
165
|
+
body = response.json()['body']
|
|
166
|
+
|
|
167
|
+
items = chat_parser.parse_chat(body)
|
|
168
|
+
if not items:
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
for item in items:
|
|
172
|
+
chats.append(item)
|
|
173
|
+
if len(chats) >= count:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
offset += len(items)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
'chats': chats,
|
|
180
|
+
'unread': unread
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def chat_messages(self, thread_id: str,
|
|
184
|
+
last_msg_id: Optional[str] = None
|
|
185
|
+
) -> List[Dict[str, Any]]:
|
|
186
|
+
"""
|
|
187
|
+
Get chat messages by `thread_id` and `last_msg_id` (so history can be
|
|
188
|
+
loaded).
|
|
189
|
+
"""
|
|
190
|
+
params = {
|
|
191
|
+
'action': 'load_messages',
|
|
192
|
+
'thread': thread_id
|
|
193
|
+
}
|
|
194
|
+
if last_msg_id:
|
|
195
|
+
params['last_msg_id'] = last_msg_id
|
|
196
|
+
|
|
197
|
+
response = self._post("/pm.php", params)
|
|
198
|
+
body = response.json()['body']
|
|
199
|
+
|
|
200
|
+
chat_parser = ChatParser()
|
|
201
|
+
messages = chat_parser.parse_messages(body)
|
|
202
|
+
|
|
203
|
+
return messages
|
|
204
|
+
|
|
205
|
+
def chat_send(self, thread_id: str, message: str):
|
|
206
|
+
"""
|
|
207
|
+
Send a new message into the `thread_id` that identifies the chat with
|
|
208
|
+
the user.
|
|
209
|
+
"""
|
|
210
|
+
params = {
|
|
211
|
+
'action': 'send_message',
|
|
212
|
+
'thread': thread_id,
|
|
213
|
+
'message': message
|
|
214
|
+
}
|
|
215
|
+
response = self._post("/pm.php", params)
|
|
216
|
+
assert '"error"' not in response.text, response.text
|
|
217
|
+
|
|
218
|
+
def chat_delete(self, thread_id: str):
|
|
219
|
+
"""
|
|
220
|
+
Delete the chat with `thread_id`.
|
|
221
|
+
"""
|
|
222
|
+
params = {
|
|
223
|
+
'action': 'delete_thread',
|
|
224
|
+
'thread': thread_id,
|
|
225
|
+
'block_user': '0'
|
|
226
|
+
}
|
|
227
|
+
self._post("/pm.php", params)
|
|
228
|
+
|
|
229
|
+
def friends(self, uid: str) -> List[Dict[str, Any]]:
|
|
230
|
+
"""
|
|
231
|
+
Get friends of the user given as `uid`.
|
|
232
|
+
"""
|
|
233
|
+
url = "/app/friends?uid={}".format(uid)
|
|
234
|
+
response = self._get(url, check_auth=True)
|
|
235
|
+
friends_parser = FriendsParser()
|
|
236
|
+
items = friends_parser.parse(response.text)
|
|
237
|
+
return items
|
|
238
|
+
|
|
239
|
+
def friend_add(self, uid: str):
|
|
240
|
+
"""
|
|
241
|
+
Send friend request to the user given by `uid`.
|
|
242
|
+
"""
|
|
243
|
+
url = "/app/friends/add?uid={}".format(uid)
|
|
244
|
+
response = self._get(url)
|
|
245
|
+
if response.status_code != 302:
|
|
246
|
+
raise APIError("could not add user")
|
|
247
|
+
|
|
248
|
+
def friend_remove(self, uid: str):
|
|
249
|
+
"""
|
|
250
|
+
Remove friend given by `uid`.
|
|
251
|
+
"""
|
|
252
|
+
url = "/app/friends/delete?uid={}".format(uid)
|
|
253
|
+
response = self._get(url)
|
|
254
|
+
if response.status_code != 302:
|
|
255
|
+
raise APIError("could not delete user")
|
|
256
|
+
|
|
257
|
+
def albums(self, uid: str) -> List[Dict[str, Any]]:
|
|
258
|
+
"""
|
|
259
|
+
Get albums of the user given by `uid`.
|
|
260
|
+
"""
|
|
261
|
+
response = self._get("/app/albums", {'uid': uid}, check_auth=True)
|
|
262
|
+
pictures_parser = PicturesParser()
|
|
263
|
+
items = pictures_parser.parse_albums(response.text)
|
|
264
|
+
return items
|
|
265
|
+
|
|
266
|
+
def pictures(self, uid: str, aid: str):
|
|
267
|
+
"""
|
|
268
|
+
Get pictures of the user (given by `uid`) and his album (given by
|
|
269
|
+
`aid`).
|
|
270
|
+
"""
|
|
271
|
+
response = self._get("/app/album", {'uid': uid, 'aid': aid},
|
|
272
|
+
check_auth=True)
|
|
273
|
+
pictures_parser = PicturesParser()
|
|
274
|
+
items = pictures_parser.parse_pictures(response.text)
|
|
275
|
+
return items
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def _get_full_url(cls, url, params=None):
|
|
279
|
+
if not url.startswith("/"):
|
|
280
|
+
url = "/" + url
|
|
281
|
+
url = "https://{}{}".format(cls.host, url)
|
|
282
|
+
if params is not None:
|
|
283
|
+
url += '?' + urlencode(params, True)
|
|
284
|
+
return url
|
|
285
|
+
|
|
286
|
+
def _get(self, url, params=None, check_auth=False):
|
|
287
|
+
headers = self._get_headers()
|
|
288
|
+
if params is not None:
|
|
289
|
+
url = url + '?' + urlencode(params, True)
|
|
290
|
+
full_url = self._get_full_url(url)
|
|
291
|
+
response = requests.get(full_url, headers=headers, timeout=self.timeout,
|
|
292
|
+
allow_redirects=False)
|
|
293
|
+
if check_auth and not self._check_body_for_auth(response.text):
|
|
294
|
+
raise APIAuthError()
|
|
295
|
+
return response
|
|
296
|
+
|
|
297
|
+
def _post(self, url, params, check_auth=False):
|
|
298
|
+
headers = self._get_headers()
|
|
299
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
300
|
+
full_url = self._get_full_url(url)
|
|
301
|
+
response = requests.post(full_url, data=params, headers=headers,
|
|
302
|
+
timeout=self.timeout, allow_redirects=False)
|
|
303
|
+
if check_auth and not self._check_body_for_auth(response.text):
|
|
304
|
+
raise APIAuthError()
|
|
305
|
+
return response
|
|
306
|
+
|
|
307
|
+
def _get_headers(self):
|
|
308
|
+
cookie = Cookie()
|
|
309
|
+
cookie.update(self._session.cookie())
|
|
310
|
+
headers = {
|
|
311
|
+
'Cookie': cookie.as_string(),
|
|
312
|
+
'User-Agent': self.user_agent
|
|
313
|
+
}
|
|
314
|
+
return headers
|
|
315
|
+
|
|
316
|
+
def _check_body_for_auth(self, body):
|
|
317
|
+
return "/app/auth/logout" in body
|
|
318
|
+
|
|
319
|
+
def _prepare_search_params(self, options):
|
|
320
|
+
params = {
|
|
321
|
+
'offset': '',
|
|
322
|
+
'sort': 'last_login',
|
|
323
|
+
'age1': options.get('age1', '16'),
|
|
324
|
+
'age2': options.get('age2', '110'),
|
|
325
|
+
'sex[]': options.get('sex', ['male', 'female']),
|
|
326
|
+
'continents[]': options.get('continents', ['AF', 'AS', 'EU', 'NA',
|
|
327
|
+
'OC', 'SA']),
|
|
328
|
+
'countries[]': options.get('countries', ['---']),
|
|
329
|
+
'languages[]': ['---'],
|
|
330
|
+
'lfor[]': [
|
|
331
|
+
'lfor_email', 'lfor_snail', 'lfor_langex',
|
|
332
|
+
'lfor_friend', 'lfor_flirt', 'lfor_relation'
|
|
333
|
+
],
|
|
334
|
+
'keywords': options.get('keywords', ''),
|
|
335
|
+
'username': '',
|
|
336
|
+
'csrf_token': 'ZjU1ZWZkM2Q=',
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if options.get('online'):
|
|
340
|
+
params['online'] = '1'
|
|
341
|
+
|
|
342
|
+
if options.get('cityName'):
|
|
343
|
+
citycode = options.get('city')
|
|
344
|
+
cityname = options.get('cityName')
|
|
345
|
+
if not citycode:
|
|
346
|
+
citycode = self._get_citycode(cityname)
|
|
347
|
+
params['city'] = citycode
|
|
348
|
+
params['cityName'] = cityname
|
|
349
|
+
|
|
350
|
+
return params
|
|
351
|
+
|
|
352
|
+
def _parse_search_result(self, body):
|
|
353
|
+
users = []
|
|
354
|
+
soup = BeautifulSoup(body, "lxml")
|
|
355
|
+
items = soup.find_all('div', class_='sResMain')
|
|
356
|
+
for item in items:
|
|
357
|
+
user = item.find('a').text
|
|
358
|
+
users.append(user)
|
|
359
|
+
return users
|
|
360
|
+
|
|
361
|
+
def _get_citycode(self, cityname):
|
|
362
|
+
response = self._get("/app/async/geoAc", {"query": cityname})
|
|
363
|
+
data = response.json()
|
|
364
|
+
return data['items'][0]['id']
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class ApiAsync(Api):
|
|
368
|
+
"""
|
|
369
|
+
Class for asynchronous API. It is inherited from [Api](#Api).
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
def __init__(self, session: SessionAsync):
|
|
373
|
+
"""
|
|
374
|
+
Create an API instance.
|
|
375
|
+
"""
|
|
376
|
+
self._session = session
|
|
377
|
+
|
|
378
|
+
async def check_auth(self) -> bool:
|
|
379
|
+
"""
|
|
380
|
+
Check whether the session is authorized.
|
|
381
|
+
"""
|
|
382
|
+
if self._session:
|
|
383
|
+
try:
|
|
384
|
+
await self._request(self._session.username)
|
|
385
|
+
except APIAuthError:
|
|
386
|
+
return False
|
|
387
|
+
else:
|
|
388
|
+
return True
|
|
389
|
+
else:
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
async def view(self, user: str):
|
|
393
|
+
"""
|
|
394
|
+
View the given user.
|
|
395
|
+
"""
|
|
396
|
+
await self._request(user)
|
|
397
|
+
|
|
398
|
+
async def profile(self, user: str) -> List[Dict[str, Any]]:
|
|
399
|
+
"""
|
|
400
|
+
Get the profile information of the user.
|
|
401
|
+
"""
|
|
402
|
+
html = await self._request(user)
|
|
403
|
+
|
|
404
|
+
if "User not found." in html:
|
|
405
|
+
raise APIError("User not found")
|
|
406
|
+
|
|
407
|
+
if "Sorry, this user's privacy settings do not allow " \
|
|
408
|
+
"you to contact them." in html:
|
|
409
|
+
raise APIError("Blocked by user")
|
|
410
|
+
|
|
411
|
+
parser = ProfileParser()
|
|
412
|
+
data = parser.parse(html)
|
|
413
|
+
|
|
414
|
+
return data
|
|
415
|
+
|
|
416
|
+
async def visitors(self) -> List[str]:
|
|
417
|
+
"""
|
|
418
|
+
Get users who viewed your profile.
|
|
419
|
+
"""
|
|
420
|
+
html = await self._request("/app/views")
|
|
421
|
+
soup = BeautifulSoup(html, "lxml")
|
|
422
|
+
items = soup.find_all('div', class_='vBottomTxt')
|
|
423
|
+
users = []
|
|
424
|
+
for item in items:
|
|
425
|
+
user = item.find('a')['href'].split('?')[0][1:]
|
|
426
|
+
users.append(user)
|
|
427
|
+
return users
|
|
428
|
+
|
|
429
|
+
async def search(self, options: dict, limit: int = 1000,
|
|
430
|
+
timeout: float = 0.0) -> Generator[str, None, None]:
|
|
431
|
+
"""
|
|
432
|
+
Search for users according to the search `options`. The total number
|
|
433
|
+
of users is `limit`. Since users are requested as pages, `timeout`
|
|
434
|
+
is a timeout between page loading.
|
|
435
|
+
"""
|
|
436
|
+
html = await self._request("/app/search")
|
|
437
|
+
csrf_token = find_csrf_token(html)
|
|
438
|
+
|
|
439
|
+
offset = 0
|
|
440
|
+
params = self._prepare_search_params(options)
|
|
441
|
+
params['csrf_token'] = csrf_token
|
|
442
|
+
while True:
|
|
443
|
+
params['offset'] = str(offset)
|
|
444
|
+
|
|
445
|
+
html = await self._request("/app/search", params=params)
|
|
446
|
+
users = self._parse_search_result(html)
|
|
447
|
+
|
|
448
|
+
if not users:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
for user in users:
|
|
452
|
+
yield user
|
|
453
|
+
offset += 1
|
|
454
|
+
if offset >= limit:
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
await asyncio.sleep(timeout)
|
|
458
|
+
|
|
459
|
+
async def get_uid(self, user: str) -> str:
|
|
460
|
+
"""
|
|
461
|
+
Get unique user identifier `uid`.
|
|
462
|
+
"""
|
|
463
|
+
profile_info = await self.profile(user)
|
|
464
|
+
return profile_info['uid']
|
|
465
|
+
|
|
466
|
+
async def get_thread_id(self, uid: str) -> str:
|
|
467
|
+
"""
|
|
468
|
+
Get a chat thread_id with a user given as `uid`.
|
|
469
|
+
"""
|
|
470
|
+
params = {'action': 'send', 'uid': uid}
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
await self._request("/pm.php", params=params)
|
|
474
|
+
except APIRedirectError as exc:
|
|
475
|
+
location = exc.args[1]
|
|
476
|
+
thread_id = parse_qs(urlparse(location).query)['thread_id'][0]
|
|
477
|
+
return thread_id
|
|
478
|
+
else:
|
|
479
|
+
raise APIError("Could not load thread_id")
|
|
480
|
+
|
|
481
|
+
async def chat(self, count: int = 9, offset: int = 0) -> Dict[str, Any]:
|
|
482
|
+
"""
|
|
483
|
+
Get general chat information in count `count` with the offset `offset`.
|
|
484
|
+
"""
|
|
485
|
+
chat_parser = ChatParser()
|
|
486
|
+
|
|
487
|
+
html = await self._request("/pm.php")
|
|
488
|
+
maxmsgid = chat_parser.parse_maxmsgid(html)
|
|
489
|
+
unread = chat_parser.parse_unread(html)
|
|
490
|
+
|
|
491
|
+
chats = []
|
|
492
|
+
while len(chats) < count:
|
|
493
|
+
params = {
|
|
494
|
+
'action': 'more_threads',
|
|
495
|
+
'from': str(offset),
|
|
496
|
+
'filter': 'all',
|
|
497
|
+
'max_msg_id': maxmsgid,
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
text = await self._request("/pm.php", params=params, method='post',
|
|
501
|
+
check_auth=False)
|
|
502
|
+
body = json.loads(text)['body']
|
|
503
|
+
|
|
504
|
+
items = chat_parser.parse_chat(body)
|
|
505
|
+
if not items:
|
|
506
|
+
break
|
|
507
|
+
|
|
508
|
+
for item in items:
|
|
509
|
+
chats.append(item)
|
|
510
|
+
if len(chats) >= count:
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
offset += len(items)
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
'chats': chats,
|
|
517
|
+
'unread': unread
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async def chat_messages(self, thread_id: str,
|
|
521
|
+
last_msg_id: Optional[str] = None
|
|
522
|
+
) -> List[Dict[str, Any]]:
|
|
523
|
+
"""
|
|
524
|
+
Get chat messages by `thread_id` and `last_msg_id` (so history can be
|
|
525
|
+
loaded).
|
|
526
|
+
"""
|
|
527
|
+
params = {
|
|
528
|
+
'action': 'load_messages',
|
|
529
|
+
'thread': thread_id
|
|
530
|
+
}
|
|
531
|
+
if last_msg_id:
|
|
532
|
+
params['last_msg_id'] = last_msg_id
|
|
533
|
+
|
|
534
|
+
text = await self._request("/pm.php", params=params, method='post',
|
|
535
|
+
check_auth=False)
|
|
536
|
+
body = json.loads(text)['body']
|
|
537
|
+
|
|
538
|
+
chat_parser = ChatParser()
|
|
539
|
+
messages = chat_parser.parse_messages(body)
|
|
540
|
+
|
|
541
|
+
return messages
|
|
542
|
+
|
|
543
|
+
async def chat_send(self, thread_id: str, message: str):
|
|
544
|
+
"""
|
|
545
|
+
Send a new message into the `thread_id` that identifies the chat with
|
|
546
|
+
the user.
|
|
547
|
+
"""
|
|
548
|
+
params = {
|
|
549
|
+
'action': 'send_message',
|
|
550
|
+
'thread': thread_id,
|
|
551
|
+
'message': message
|
|
552
|
+
}
|
|
553
|
+
text = await self._request("/pm.php", params=params, method='post',
|
|
554
|
+
check_auth=False)
|
|
555
|
+
if '"error"' in text:
|
|
556
|
+
raise APIError(text)
|
|
557
|
+
|
|
558
|
+
async def chat_delete(self, thread_id: str):
|
|
559
|
+
"""
|
|
560
|
+
Delete the chat with `thread_id`.
|
|
561
|
+
"""
|
|
562
|
+
params = {
|
|
563
|
+
'action': 'delete_thread',
|
|
564
|
+
'thread': thread_id,
|
|
565
|
+
'block_user': '0'
|
|
566
|
+
}
|
|
567
|
+
await self._request("/pm.php", params=params, method='post',
|
|
568
|
+
check_auth=False)
|
|
569
|
+
|
|
570
|
+
async def friends(self, uid: str) -> List[Dict[str, Any]]:
|
|
571
|
+
"""
|
|
572
|
+
Get friends of the user given as `uid`.
|
|
573
|
+
"""
|
|
574
|
+
url = "/app/friends?uid={}".format(uid)
|
|
575
|
+
html = await self._request(url)
|
|
576
|
+
friends_parser = FriendsParser()
|
|
577
|
+
items = friends_parser.parse(html)
|
|
578
|
+
return items
|
|
579
|
+
|
|
580
|
+
async def friend_add(self, uid: str):
|
|
581
|
+
"""
|
|
582
|
+
Send friend request to the user given by `uid`.
|
|
583
|
+
"""
|
|
584
|
+
url = "/app/friends/add?uid={}".format(uid)
|
|
585
|
+
try:
|
|
586
|
+
await self._request(url)
|
|
587
|
+
except APIRedirectError:
|
|
588
|
+
pass
|
|
589
|
+
else:
|
|
590
|
+
raise APIError("Could not add friend")
|
|
591
|
+
|
|
592
|
+
async def friend_remove(self, uid: str):
|
|
593
|
+
"""
|
|
594
|
+
Remove friend given by `uid`.
|
|
595
|
+
"""
|
|
596
|
+
url = "/app/friends/delete?uid={}".format(uid)
|
|
597
|
+
try:
|
|
598
|
+
await self._request(url)
|
|
599
|
+
except APIRedirectError:
|
|
600
|
+
pass
|
|
601
|
+
else:
|
|
602
|
+
raise APIError("Could not delete friend")
|
|
603
|
+
|
|
604
|
+
async def albums(self, uid: str) -> List[Dict[str, Any]]:
|
|
605
|
+
"""
|
|
606
|
+
Get albums of the user given by `uid`.
|
|
607
|
+
"""
|
|
608
|
+
html = await self._request("/app/albums", params={'uid': uid})
|
|
609
|
+
pictures_parser = PicturesParser()
|
|
610
|
+
items = pictures_parser.parse_albums(html)
|
|
611
|
+
return items
|
|
612
|
+
|
|
613
|
+
async def pictures(self, uid: str, aid: str):
|
|
614
|
+
"""
|
|
615
|
+
Get pictures of the user (given by `uid`) and his album (given by
|
|
616
|
+
`aid`).
|
|
617
|
+
"""
|
|
618
|
+
html = await self._request("/app/album",
|
|
619
|
+
params={'uid': uid, 'aid': aid})
|
|
620
|
+
pictures_parser = PicturesParser()
|
|
621
|
+
items = pictures_parser.parse_pictures(html)
|
|
622
|
+
return items
|
|
623
|
+
|
|
624
|
+
async def _request(self, url, method='get', params=None, check_auth=True):
|
|
625
|
+
assert method in ('get', 'post')
|
|
626
|
+
|
|
627
|
+
full_url = self._get_full_url(url, params if method == 'get' else None)
|
|
628
|
+
headers = self._get_headers()
|
|
629
|
+
|
|
630
|
+
if method == 'post':
|
|
631
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
632
|
+
|
|
633
|
+
kwargs = {
|
|
634
|
+
'url': full_url,
|
|
635
|
+
'headers': headers,
|
|
636
|
+
'timeout': self.timeout,
|
|
637
|
+
'allow_redirects': False,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if method == 'post':
|
|
641
|
+
kwargs['data'] = params
|
|
642
|
+
|
|
643
|
+
async with aiohttp.ClientSession() as session:
|
|
644
|
+
request_func = session.post if method == 'post' else session.get
|
|
645
|
+
async with request_func(**kwargs) as resp:
|
|
646
|
+
if resp.status in (301, 302):
|
|
647
|
+
raise APIRedirectError(
|
|
648
|
+
resp.status, resp.headers['Location']
|
|
649
|
+
)
|
|
650
|
+
body = await resp.text()
|
|
651
|
+
|
|
652
|
+
if check_auth and not self._check_body_for_auth(body):
|
|
653
|
+
raise APIAuthError()
|
|
654
|
+
|
|
655
|
+
return body
|
interpals_api/cookie.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A simple class for Cookie support. It keeps cookie data as a dictionary and
|
|
3
|
+
has special functions to convert from and into strings and to parse 'Set-Cookie'
|
|
4
|
+
header.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Tuple, Dict, Self
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Cookie(dict):
|
|
11
|
+
"""
|
|
12
|
+
A class that manages HTTP cookies.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def as_string(self) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Represents saves cookie values as a string for HTTP header.
|
|
18
|
+
"""
|
|
19
|
+
return "; ".join(
|
|
20
|
+
"{}={}".format(key, value) for key, value in self.items()
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return self.as_string()
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def parse_set_cookie(cls, set_cookie: str) -> Tuple[str, str]:
|
|
28
|
+
"""
|
|
29
|
+
Extracts key and value of a cookie from Set-Cookie header of an HTTP
|
|
30
|
+
response.
|
|
31
|
+
"""
|
|
32
|
+
key, value = set_cookie[:set_cookie.find(';')].split('=')
|
|
33
|
+
key = key.strip()
|
|
34
|
+
value = value.strip()
|
|
35
|
+
return key, value
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_response_headers(cls, headers: Dict[str, str]) -> Self:
|
|
39
|
+
"""
|
|
40
|
+
Creates cookie instance from HTTP response headers.
|
|
41
|
+
"""
|
|
42
|
+
obj = cls()
|
|
43
|
+
for key, value in headers.items():
|
|
44
|
+
if key.lower() == "set-cookie":
|
|
45
|
+
for val in value.split("HttpOnly,"):
|
|
46
|
+
cookie_key, cookie_value = cls.parse_set_cookie(val)
|
|
47
|
+
obj[cookie_key] = cookie_value
|
|
48
|
+
return obj
|