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