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/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
@@ -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