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/errors.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The project error tree.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class InterpalsAPIError(Exception):
|
|
6
|
+
"""
|
|
7
|
+
Base InterpalsAPI error.
|
|
8
|
+
"""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SessionError(InterpalsAPIError):
|
|
13
|
+
"""
|
|
14
|
+
General session error.
|
|
15
|
+
"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class APIError(InterpalsAPIError):
|
|
20
|
+
"""
|
|
21
|
+
General API error.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NoCSRFTokenError(SessionError):
|
|
27
|
+
"""
|
|
28
|
+
CSRF token not found on session create.
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WrongUsernameOrPasswordError(SessionError):
|
|
34
|
+
"""
|
|
35
|
+
Invalid credentials on session create.
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TooManyLoginAttemptsError(SessionError):
|
|
41
|
+
"""
|
|
42
|
+
Too many attempts on session create.
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class APITimeoutError(APIError):
|
|
48
|
+
"""
|
|
49
|
+
Timeout error in API call.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class APIAuthError(APIError):
|
|
55
|
+
"""
|
|
56
|
+
Authorization error in API call.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class APIRedirectError(APIError):
|
|
62
|
+
"""
|
|
63
|
+
Redirect error in API call.
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser for the chat. It supports flexible chats browsing, general information
|
|
3
|
+
and fetching the messages from the history.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChatParser:
|
|
12
|
+
"""
|
|
13
|
+
Chat parser:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
parser = ChatParser()
|
|
17
|
+
maxmsgid = parser.parse_maxmsgid(html)
|
|
18
|
+
unread = parser.parse_unread(html)
|
|
19
|
+
chats = parser.parse_chat(html)
|
|
20
|
+
messages = parser.parse_messages(html)
|
|
21
|
+
```
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def parse_maxmsgid(self, content: str) -> int:
|
|
25
|
+
"""
|
|
26
|
+
Parse maximum id of existing chats from HTML content. It is needed to
|
|
27
|
+
request chats up to this number (as `max_msg_id` parameter).
|
|
28
|
+
"""
|
|
29
|
+
soup = BeautifulSoup(content, "lxml")
|
|
30
|
+
maxmsgid = soup.find('div', id='threads_left')['data-max-msg-id']
|
|
31
|
+
return int(maxmsgid)
|
|
32
|
+
|
|
33
|
+
def parse_unread(self, content: str) -> int:
|
|
34
|
+
"""
|
|
35
|
+
Parse the count of unread chats from HTML content.
|
|
36
|
+
"""
|
|
37
|
+
soup = BeautifulSoup(content, "lxml")
|
|
38
|
+
unread = soup.find('span', id='pmNewCnt').text.strip()
|
|
39
|
+
return int(unread[2:-1]) if unread else 0
|
|
40
|
+
|
|
41
|
+
def parse_chat(self, content: str) -> List[Dict[str, Any]]:
|
|
42
|
+
"""
|
|
43
|
+
Parse all chats from HTML content. They include thread_id (it's needed
|
|
44
|
+
to request chat messages) and other basic fields. The result is a list
|
|
45
|
+
of dictionaries.
|
|
46
|
+
"""
|
|
47
|
+
soup = BeautifulSoup(content, "lxml")
|
|
48
|
+
|
|
49
|
+
chats = []
|
|
50
|
+
elements = soup.find_all('div', class_='pm_thread')
|
|
51
|
+
for element in elements:
|
|
52
|
+
chat = {}
|
|
53
|
+
chat.update(self._parse_thread_info(element))
|
|
54
|
+
chat.update(self._parse_user_info(element))
|
|
55
|
+
chat.update(self._parse_message_info(element))
|
|
56
|
+
chats.append(chat)
|
|
57
|
+
|
|
58
|
+
return chats
|
|
59
|
+
|
|
60
|
+
def parse_messages(self, content: str) -> List[Dict[str, Any]]:
|
|
61
|
+
"""
|
|
62
|
+
Parse char messages from HTML content.
|
|
63
|
+
"""
|
|
64
|
+
soup = BeautifulSoup(content, "lxml")
|
|
65
|
+
|
|
66
|
+
messages = []
|
|
67
|
+
|
|
68
|
+
elements = soup.find_all('div', class_=['pm_date', 'pm_msg'])
|
|
69
|
+
user = None
|
|
70
|
+
date = None
|
|
71
|
+
for element in elements:
|
|
72
|
+
if 'pm_date' in element['class']:
|
|
73
|
+
date = element.text
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
msg_user_thumb = element.find('div', class_='msg_user_thumb')
|
|
77
|
+
if msg_user_thumb:
|
|
78
|
+
user = msg_user_thumb.a['href']
|
|
79
|
+
|
|
80
|
+
message = {}
|
|
81
|
+
message.update(self._parse_message(element))
|
|
82
|
+
message['date'] = date
|
|
83
|
+
message['user'] = user
|
|
84
|
+
messages.append(message)
|
|
85
|
+
|
|
86
|
+
return messages
|
|
87
|
+
|
|
88
|
+
def _parse_thread_info(self, element):
|
|
89
|
+
data = {}
|
|
90
|
+
data['new'] = 'new' in element['class']
|
|
91
|
+
data['thread_id'] = element['id'][7:]
|
|
92
|
+
return data
|
|
93
|
+
|
|
94
|
+
def _parse_user_info(self, element):
|
|
95
|
+
data = {}
|
|
96
|
+
|
|
97
|
+
tui_el = element.find_all('div', class_='tui_el')
|
|
98
|
+
user_age = tui_el[0].text.strip().split(', ')
|
|
99
|
+
if len(user_age) == 2:
|
|
100
|
+
data['user'], data['age'] = user_age
|
|
101
|
+
else:
|
|
102
|
+
data['user'], data['age'] = user_age[0], None
|
|
103
|
+
data['city'] = tui_el[1].text if len(tui_el) > 1 else None
|
|
104
|
+
|
|
105
|
+
tui_el_0_class = tui_el[0]['class']
|
|
106
|
+
data['sex'] = tui_el_0_class[1] if len(tui_el_0_class) > 1 else None
|
|
107
|
+
|
|
108
|
+
data['avatar'] = 'https:' + element.find(class_='thumb')['src']
|
|
109
|
+
|
|
110
|
+
data['flag'] = None
|
|
111
|
+
flag_img = element.find('div', class_='tui_flag').img
|
|
112
|
+
if flag_img:
|
|
113
|
+
data['flag'] = 'https:' + flag_img['src']
|
|
114
|
+
|
|
115
|
+
data['online'] = bool(element.find(class_='online-now'))
|
|
116
|
+
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
def _parse_message_info(self, element):
|
|
120
|
+
data = {}
|
|
121
|
+
data['message'] = element.find(class_='th_snippet').text.strip()
|
|
122
|
+
data['i_sent'] = bool(element.find(class_='snippet_thumb'))
|
|
123
|
+
data['unread'] = 'pm_new' in element.find(class_='th_snippet')['class']
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
def _parse_message(self, element):
|
|
127
|
+
data = {}
|
|
128
|
+
data['time'] = element.find('div', class_='pm_time').text
|
|
129
|
+
data['text'] = element.find('div', class_='msg_body').text.strip()
|
|
130
|
+
data['msg_id'] = element['id'][4:]
|
|
131
|
+
data['unread'] = 'pm_unread' in element['class']
|
|
132
|
+
return data
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser for user friends.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FriendsParser:
|
|
11
|
+
"""
|
|
12
|
+
Friends parser:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
parser = FriendsParser()
|
|
16
|
+
data = parser.parse(html)
|
|
17
|
+
```
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def parse(self, content: str) -> List[Dict[str, Any]]:
|
|
21
|
+
"""
|
|
22
|
+
Parse HTML content and return list of friends as dictionaries having
|
|
23
|
+
username, age, city, online status and avatar URL.
|
|
24
|
+
"""
|
|
25
|
+
soup = BeautifulSoup(content, "lxml")
|
|
26
|
+
|
|
27
|
+
boxes = soup.find_all('div', class_='friendBox')
|
|
28
|
+
friends = []
|
|
29
|
+
for box in boxes:
|
|
30
|
+
friend = {}
|
|
31
|
+
friend.update(self._parse_user_info(box))
|
|
32
|
+
friend.update(self._parse_avatar(box))
|
|
33
|
+
friend.update(self._parse_online(box))
|
|
34
|
+
friends.append(friend)
|
|
35
|
+
|
|
36
|
+
return friends
|
|
37
|
+
|
|
38
|
+
def _parse_avatar(self, box):
|
|
39
|
+
avatar = 'https:' + box.img['src']
|
|
40
|
+
return {'avatar': avatar}
|
|
41
|
+
|
|
42
|
+
def _parse_user_info(self, box):
|
|
43
|
+
src = box.find('img', class_='status')['src']
|
|
44
|
+
online = 'online' in src
|
|
45
|
+
return {'online': online}
|
|
46
|
+
|
|
47
|
+
def _parse_online(self, box):
|
|
48
|
+
username, age, city, *extra = box.text.strip().split()
|
|
49
|
+
return {
|
|
50
|
+
'user': username,
|
|
51
|
+
'age': age,
|
|
52
|
+
'city': city
|
|
53
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser for user pictures. It supports the album structure.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PicturesParser:
|
|
11
|
+
"""
|
|
12
|
+
Pictures parser:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
parser = PicturesParser()
|
|
16
|
+
albums_data = parser.parse_albums(albums_html)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
parser = PicturesParser()
|
|
21
|
+
pictures_data = parser.parse_pictures(pictures_html)
|
|
22
|
+
```
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def parse_albums(self, content: str) -> List[Dict[str, Any]]:
|
|
26
|
+
"""
|
|
27
|
+
Parse HTML content and return album information incliding its `aid`,
|
|
28
|
+
description and pictures' URLs.
|
|
29
|
+
"""
|
|
30
|
+
soup = BeautifulSoup(content, "lxml")
|
|
31
|
+
|
|
32
|
+
elements = soup.find_all('div', class_='editAlbumBox')
|
|
33
|
+
albums = []
|
|
34
|
+
for element in elements:
|
|
35
|
+
album = {}
|
|
36
|
+
album.update(self._parse_aid(element))
|
|
37
|
+
album.update(self._parse_info(element))
|
|
38
|
+
album.update(self._parse_album_pictures(element))
|
|
39
|
+
albums.append(album)
|
|
40
|
+
|
|
41
|
+
return albums
|
|
42
|
+
|
|
43
|
+
def parse_pictures(self, content: str) -> List[Dict[str, Any]]:
|
|
44
|
+
"""
|
|
45
|
+
Parse HTML content and return a list of pictures as URLs of small and
|
|
46
|
+
big ones.
|
|
47
|
+
"""
|
|
48
|
+
soup = BeautifulSoup(content, "lxml")
|
|
49
|
+
|
|
50
|
+
elements = soup.find_all('div', class_='albThumb')
|
|
51
|
+
pictures = []
|
|
52
|
+
for element in elements:
|
|
53
|
+
picture = {}
|
|
54
|
+
picture['src180x180'] = element.find('img')['src']
|
|
55
|
+
picture['src'] = ("https://ipstatic.net/photos/"
|
|
56
|
+
+ picture['src180x180'].split('/180x180/')[1])
|
|
57
|
+
# picture['src'] = self._patch_src(picture['src'])
|
|
58
|
+
pictures.append(picture)
|
|
59
|
+
|
|
60
|
+
return pictures
|
|
61
|
+
|
|
62
|
+
def _parse_aid(self, element):
|
|
63
|
+
href = element.find('a', class_='albEditThumb')['href']
|
|
64
|
+
aid = href.split('&')[0].split('=')[-1]
|
|
65
|
+
return {'aid': aid}
|
|
66
|
+
|
|
67
|
+
def _parse_info(self, element):
|
|
68
|
+
name = element.find('h3').text
|
|
69
|
+
stat = element.find('div', class_='albumStats').text.split('|')
|
|
70
|
+
pics_number = int(stat[0].strip().split()[0])
|
|
71
|
+
created = stat[1].strip().split(None, 1)[1]
|
|
72
|
+
updated = stat[2].strip().split(None, 1)[1]
|
|
73
|
+
return {
|
|
74
|
+
'name': name,
|
|
75
|
+
'pics_number': pics_number,
|
|
76
|
+
'created': created,
|
|
77
|
+
'updated': updated
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def _parse_album_pictures(self, element):
|
|
81
|
+
links = element.find_all('a', class_='thumb')
|
|
82
|
+
pics = [
|
|
83
|
+
link.find('img')['src']
|
|
84
|
+
for link in links
|
|
85
|
+
]
|
|
86
|
+
return {'pictures': pics}
|
|
87
|
+
|
|
88
|
+
# def _patch_src(self, src):
|
|
89
|
+
# if src[-6] == '_':
|
|
90
|
+
# src = src[:-6] + src[-4:]
|
|
91
|
+
# return src
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser for general profile data of a user. It includes gender, location,
|
|
3
|
+
online, join date, status message, avatar URL, uid number and the description.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProfileParser:
|
|
12
|
+
"""
|
|
13
|
+
Parser class.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
parser = ProfileParser()
|
|
17
|
+
data = parser.parse(html)
|
|
18
|
+
```
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def parse(self, content: str) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Parse HTML content and return the data as a dictionary.
|
|
24
|
+
"""
|
|
25
|
+
soup = BeautifulSoup(content, "lxml")
|
|
26
|
+
|
|
27
|
+
data = {}
|
|
28
|
+
|
|
29
|
+
data.update(self._extract_name_age_sex(soup))
|
|
30
|
+
data.update(self._extract_location(soup))
|
|
31
|
+
data.update(self._extract_online(soup))
|
|
32
|
+
data.update(self._extract_joined_updated(soup))
|
|
33
|
+
data.update(self._extract_status(soup))
|
|
34
|
+
data.update(self._extract_avatar(soup))
|
|
35
|
+
data.update(self._extract_uid(soup))
|
|
36
|
+
data.update(self._extract_info(soup))
|
|
37
|
+
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
def _extract_name_age_sex(self, soup):
|
|
41
|
+
profile_box = soup.find(class_='profileBox')
|
|
42
|
+
|
|
43
|
+
sex = profile_box.find('img')['src'].split('/')[-1][:-7]
|
|
44
|
+
|
|
45
|
+
pieces = profile_box.text.split('\n')
|
|
46
|
+
pieces = list(map(str.strip, pieces))
|
|
47
|
+
pieces = list(filter(lambda s: s, pieces))
|
|
48
|
+
piece = pieces[1]
|
|
49
|
+
piece = piece.replace('y.o.', '')
|
|
50
|
+
na = piece.split(', ')
|
|
51
|
+
if len(na) == 2:
|
|
52
|
+
name, age = na
|
|
53
|
+
else:
|
|
54
|
+
if na.isdigit():
|
|
55
|
+
name, age = None, na
|
|
56
|
+
else:
|
|
57
|
+
name, age = na, None
|
|
58
|
+
age = int(age)
|
|
59
|
+
|
|
60
|
+
return {'name': name, 'age': age, 'sex': sex}
|
|
61
|
+
|
|
62
|
+
def _extract_location(self, soup):
|
|
63
|
+
links = soup.find(class_='profLocation').find_all('a')
|
|
64
|
+
city = links[1].text
|
|
65
|
+
city_code = links[1]['href'].split('=')[-1]
|
|
66
|
+
country = links[2].text
|
|
67
|
+
country_code = links[2]['href'][-2:]
|
|
68
|
+
return {
|
|
69
|
+
'city': city,
|
|
70
|
+
'city_code': city_code,
|
|
71
|
+
'country': country,
|
|
72
|
+
'country_code': country_code
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def _extract_online(self, soup):
|
|
76
|
+
text = soup.find(class_='profOnlineStatus').text.strip()
|
|
77
|
+
if text.lower() == "online now":
|
|
78
|
+
online = True
|
|
79
|
+
last = None
|
|
80
|
+
else:
|
|
81
|
+
online = False
|
|
82
|
+
last = ' '.join(text.split()[1:])
|
|
83
|
+
return {'online': online, 'last': last}
|
|
84
|
+
|
|
85
|
+
def _extract_joined_updated(self, soup):
|
|
86
|
+
text = soup.find(class_='profileBox').find_all('p')[0].text.strip()
|
|
87
|
+
joined, updated = text.split('\n')[1::2]
|
|
88
|
+
joined = joined[:-1]
|
|
89
|
+
updated = updated[:-1]
|
|
90
|
+
return {'joined': joined, 'updated': updated}
|
|
91
|
+
|
|
92
|
+
def _extract_status(self, soup):
|
|
93
|
+
element = soup.find('span', id='prStatMsgTxt')
|
|
94
|
+
status = element.text if element else ''
|
|
95
|
+
return {'status': status}
|
|
96
|
+
|
|
97
|
+
def _extract_avatar(self, soup):
|
|
98
|
+
avatar_link = soup.find('a', class_='mainPhoto')
|
|
99
|
+
if not avatar_link:
|
|
100
|
+
avatar_link = soup.find('a', class_='mpImgLink')
|
|
101
|
+
avatar = 'https:' + avatar_link.find('img')['src']
|
|
102
|
+
return {'avatar': avatar}
|
|
103
|
+
|
|
104
|
+
def _extract_uid(self, soup):
|
|
105
|
+
report_link = soup.find('a', class_='profReportLink')
|
|
106
|
+
if report_link:
|
|
107
|
+
uid = report_link['user-id']
|
|
108
|
+
else:
|
|
109
|
+
uid = soup.find('div', class_='hidden-uid').text
|
|
110
|
+
return {'uid': uid}
|
|
111
|
+
|
|
112
|
+
def _extract_info(self, soup):
|
|
113
|
+
box = soup.find(class_='profDataBox')
|
|
114
|
+
titles = box.find_all('h2')
|
|
115
|
+
texts = box.find_all('div', class_='profDataBoxText')
|
|
116
|
+
info = []
|
|
117
|
+
for title, text in zip(titles, texts):
|
|
118
|
+
title = title.text.strip()
|
|
119
|
+
text = text.text.strip()
|
|
120
|
+
info.append({
|
|
121
|
+
'title': title,
|
|
122
|
+
'text': text
|
|
123
|
+
})
|
|
124
|
+
return {'info': info}
|