Small changes in diaspy/streams (documentation and initialization
[diaspy.git] / diaspy / settings.py
1 """This module provides access to user's settings on Diaspora*.
2 """
3
4
5 import json
6 import os
7 import re
8 import urllib
9 import warnings
10
11 from diaspy import errors, streams
12
13
14 class Account():
15 """Provides interface to account settings.
16 """
17 email_regexp = re.compile('<input id="user_email" name="user\[email\]" size="30" type="text" value="(.+?)"')
18 language_option_regexp = re.compile('<option value="([-_a-z]+)">(.*?)</option>')
19
20 def __init__(self, connection):
21 self._connection = connection
22
23 def downloadxml(self):
24 """Returns downloaded XML.
25 """
26 request = self._connection.get('user/export')
27 return request.text
28
29 def downloadPhotos(self, size='large', path='.', mark_nsfw=True, _critical=False, _stream=None):
30 """Downloads photos into the current working directory.
31 Sizes are: large, medium, small.
32 Filename is: {post_guid}_{photo_guid}.{extension}
33
34 Normally, this method will catch urllib-generated errors and
35 just issue warnings about photos that couldn't be downloaded.
36 However, with _critical param set to True errors will become
37 critical - the will be reraised in finally block.
38
39 :param size: size of the photos to download - large, medium or small
40 :type size: str
41 :param path: path to download (defaults to current working directory
42 :type path: str
43 :param mark_nsfw: will append '-nsfw' to images from posts marked as nsfw,
44 :type mark_nsfw: bool
45 :param _stream: diaspy.streams.Generic-like object (only for testing)
46 :param _critical: if True urllib errors will be reraised after generating a warning (may be removed)
47
48 :returns: integer, number of photos downloaded
49 """
50 photos = 0
51 if _stream is None:
52 stream = streams.Activity(self._connection)
53 stream.full()
54 else:
55 stream = _stream
56 for i, post in enumerate(stream):
57 if post['nsfw'] is not False: nsfw = '-nsfw'
58 else: nsfw = ''
59 if post['photos']:
60 for n, photo in enumerate(post['photos']):
61 # photo format -- .jpg, .png etc.
62 ext = photo['sizes'][size].split('.')[-1]
63 name = '{0}_{1}{2}.{3}'.format(post['guid'], photo['guid'], nsfw, ext)
64 filename = os.path.join(path, name)
65 try:
66 urllib.request.urlretrieve(url=photo['sizes'][size], filename=filename)
67 except (urllib.error.HTTPError, urllib.error.URLError) as e:
68 warnings.warn('downloading image {0} from post {1}: {2}'.format(photo['guid'], post['guid'], e))
69 finally:
70 if _critical: raise
71 photos += 1
72 return photos
73
74 def setEmail(self, email):
75 """Changes user's email.
76 """
77 data = {'_method': 'put', 'utf8': '✓', 'user[email]': email, 'authenticity_token': repr(self._connection)}
78 request = self._connection.post('user', data=data, allow_redirects=False)
79 if request.status_code != 302:
80 raise errors.SettingsError('setting email failed: {0}'.format(request.status_code))
81
82 def getEmail(self):
83 """Returns currently used email.
84 """
85 data = self._connection.get('user/edit')
86 email = self.email_regexp.search(data.text)
87 if email is None: email = ''
88 else: email = email.group(1)
89 return email
90
91 def setLanguage(self, lang):
92 """Changes user's email.
93
94 :param lang: language identifier from getLanguages()
95 """
96 data = {'_method': 'put', 'utf8': '✓', 'user[language]': lang, 'authenticity_token': repr(self._connection)}
97 request = self._connection.post('user', data=data, allow_redirects=False)
98 if request.status_code != 302:
99 raise errors.SettingsError('setting language failed: {0}'.format(request.status_code))
100
101 def getLanguages(self):
102 """Returns a list of tuples containing ('Language name', 'identifier').
103 One of the Black Magic(tm) methods.
104 """
105 request = self._connection.get('user/edit')
106 return self.language_option_regexp.findall(request.text)
107
108
109 class Privacy():
110 """Provides interface to provacy settings.
111 """
112 def __init__(self, connection):
113 self._connection = connection
114
115
116 class Profile():
117 """Provides interface to profile settigns.
118
119 WARNING:
120
121 Because of the way update requests for profile are created every field must be sent.
122 The `load()` method is used to load all information into the dictionary.
123 Setters can then be used to adjust the data.
124 Finally, `update()` can be called to send data back to pod.
125 """
126 firstname_regexp = re.compile('id="profile_first_name" name="profile\[first_name\]" type="text" value="(.*?)" />')
127 lastname_regexp = re.compile('id="profile_last_name" name="profile\[last_name\]" type="text" value="(.*?)" />')
128 bio_regexp = re.compile('<textarea id="profile_bio" name="profile\[bio\]" placeholder="Fill me out" rows="5">\n(.*?)</textarea>')
129 location_regexp = re.compile('id="profile_location" name="profile\[location\]" placeholder="Fill me out" type="text" value="(.*?)" />')
130 gender_regexp = re.compile('id="profile_gender" name="profile\[gender\]" placeholder="Fill me out" type="text" value="(.*?)" />')
131 birth_year_regexp = re.compile('selected="selected" value="([0-9]{4,4})">[0-9]{4,4}</option>')
132 birth_month_regexp = re.compile('selected="selected" value="([0-9]{1,2})">(.*?)</option>')
133 birth_day_regexp = re.compile('selected="selected" value="([0-9]{1,2})">[0-9]{1,2}</option>')
134 is_searchable_regexp = re.compile('checked="checked" id="profile_searchable" name="profile\[searchable\]" type="checkbox" value="(.*?)" />')
135 is_nsfw_regexp = re.compile('checked="checked" id="profile_nsfw" name="profile\[nsfw\]" type="checkbox" value="(.*?)" />')
136
137 def __init__(self, connection):
138 self._connection = connection
139 self.data = {'utf-8': '✓',
140 '_method': 'put',
141 'profile[first_name]': '',
142 'profile[last_name]': '',
143 'profile[tag_string]': '',
144 'tags': '',
145 'file': '',
146 'profile[bio]': '',
147 'profile[location]': '',
148 'profile[gender]': '',
149 'profile[date][year]': '',
150 'profile[date][month]': '',
151 'profile[date][day]': '',
152 }
153 self._html = self._fetchhtml()
154 self._loaded = False
155
156 def _fetchhtml(self):
157 """Fetches html that will be used to extract data.
158 """
159 return self._connection.get('profile/edit').text
160
161 def getName(self):
162 """Returns two-tuple: (first, last) name.
163 """
164 first = self.firstname_regexp.search(self._html).group(1)
165 last = self.lastname_regexp.search(self._html).group(1)
166 return (first, last)
167
168 def getTags(self):
169 """Returns tags user had selected when describing him/her-self.
170 """
171 guid = self._connection.getUserInfo()['guid']
172 html = self._connection.get('people/{0}'.format(guid)).text
173 description_regexp = re.compile('<a href="/tags/(.*?)" class="tag">#.*?</a>')
174 return [tag.lower() for tag in re.findall(description_regexp, html)]
175
176 def getBio(self):
177 """Returns user bio.
178 """
179 bio = self.bio_regexp.search(self._html).group(1)
180 return bio
181
182 def getLocation(self):
183 """Returns location string.
184 """
185 location = self.location_regexp.search(self._html).group(1)
186 return location
187
188 def getGender(self):
189 """Returns location string.
190 """
191 gender = self.gender_regexp.search(self._html).group(1)
192 return gender
193
194 def getBirthDate(self, named_month=False):
195 """Returns three-tuple: (year, month, day).
196
197 :param named_month: if True, return name of the month instead of integer
198 :type named_month: bool
199 """
200 year = self.birth_year_regexp.search(self._html)
201 if year is None: year = -1
202 else: year = int(year.group(1))
203 month = self.birth_month_regexp.search(self._html)
204 if month is None:
205 if named_month: month = ''
206 else: month = -1
207 else:
208 if named_month:
209 month = month.group(2)
210 else:
211 month = int(month.group(1))
212 day = self.birth_day_regexp.search(self._html)
213 if day is None: day = -1
214 else: day = int(day.group(1))
215 return (year, month, day)
216
217 def isSearchable(self):
218 """Returns True if profile is searchable.
219 """
220 searchable = self.is_searchable_regexp.search(self._html)
221 # this is because value="true" in every case so we just
222 # check if the field is "checked"
223 if searchable is None: searchable = False # if it isn't - the regexp just won't match
224 else: searchable = True
225 return searchable
226
227 def isNSFW(self):
228 """Returns True if profile is marked as NSFW.
229 """
230 nsfw = self.is_nsfw_regexp.search(self._html)
231 if nsfw is None: nsfw = False
232 else: nsfw = True
233 return nsfw
234
235 def setName(self, first, last):
236 """Set first and last name.
237 """
238 self.data['profile[first_name]'] = first
239 self.data['profile[last_name]'] = last
240
241 def setTags(self, tags):
242 """Sets tags that describe the user.
243 """
244 self.data['tags'] = ', '.join(['#{}'.format(tag) for tag in tags])
245
246 def setBio(self, bio):
247 """Set bio of a user.
248 """
249 self.data['profile[bio]'] = bio
250
251 def setLocation(self, location):
252 """Set location of a user.
253 """
254 self.data['profile[location]'] = location
255
256 def setGender(self, gender):
257 """Set gender of a user.
258 """
259 self.data['profile[gender]'] = gender
260
261 def setBirthDate(self, year, month, day):
262 """Set birth date of a user.
263 """
264 self.data['profile[date][year]'] = year
265 self.data['profile[date][month]'] = month
266 self.data['profile[date][day]'] = day
267
268 def setSearchable(self, searchable):
269 """Set user's searchable status.
270 """
271 self.data['profile[searchable]'] = json.dumps(searchable)
272
273 def setNSFW(self, nsfw):
274 """Set user NSFW status.
275 """
276 self.data['profile[nsfw]'] = json.dumps(nsfw)
277
278 def load(self):
279 """Loads profile data into self.data dictionary.
280 **Notice:** Not all keys are loaded yet.
281 """
282 self.setName(*self.getName())
283 self.setBio(self.getBio())
284 self.setLocation(self.getLocation())
285 self.setGender(self.getGender())
286 self.setBirthDate(*self.getBirthDate(named_month=False))
287 self.setSearchable(self.isSearchable())
288 self.setNSFW(self.isNSFW())
289 self.setTags(self.getTags())
290 self._loaded = True
291
292 def update(self):
293 """Updates profile information.
294 """
295 if not self._loaded: raise errors.DiaspyError('profile was not loaded')
296 self.data['authenticity_token'] = repr(self._connection)
297 print(self.data)
298 request = self._connection.post('profile', data=self.data, allow_redirects=False)
299 return request.status_code
300
301
302 class Services():
303 """Provides interface to services settings.
304 """
305 def __init__(self, connection):
306 self._connection = connection