Merge branch 'users'
[diaspy.git] / diaspy / people.py
1 import re
2 from diaspy.streams import Outer
3 from diaspy.models import Aspect
4 from diaspy import errors
5
6
7 class User():
8 """This class abstracts a D* user.
9 This object goes around the limitations of current D* API and will
10 extract user data using black magic.
11 However, no chickens are harmed when you use it.
12
13 The parameter fetch should be either 'posts', 'data' or 'none'. By
14 default it is 'posts' which means in addition to user data, stream
15 will be fetched. If user has not posted yet diaspy will not be able
16 to extract the information from his/her posts. Since there is no official
17 way to do it we rely on user posts. If this will be the case user
18 will be notified with appropriate exception message.
19
20 If fetch is 'data', only user data will be fetched. If the user is
21 not found, no exception will be returned.
22
23 When creating new User() one can pass either guid, handle and/or id as
24 optional parameters. GUID takes precedence over handle when fetching
25 user stream. When fetching user data, handle is required.
26 """
27 data = {}
28 stream = []
29
30 def __init__(self, connection, guid='', handle='', fetch='posts', id=0):
31 self._connection = connection
32 self.data = {
33 'guid': guid,
34 'handle': handle,
35 'id': id,
36 }
37 self._do_fetch(fetch)
38
39 def __getitem__(self, key):
40 return self.data[key]
41
42 def __str__(self):
43 return self['guid']
44
45 def __repr__(self):
46 return '{0} ({1})'.format(self['diaspora_name'], self['guid'])
47
48 def _do_fetch(self, fetch):
49 if fetch == 'posts':
50 if self['handle'] and self['guid']: self.fetchguid()
51 elif self['guid'] and not self['handle']: self.fetchguid()
52 elif self['handle'] and not self['guid']: self.fetchhandle()
53 elif fetch == 'data' and len(self['handle']):
54 self.fetchprofile()
55
56 def _sephandle(self):
57 """Separate D* handle into pod pod and user.
58
59 :returns: two-tuple (pod, user)
60 """
61 if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', self['handle']) is None:
62 raise Exception('invalid handle: {0}'.format(self['handle']))
63 handle = self['handle'].split('@')
64 pod, user = handle[1], handle[0]
65 return (pod, user)
66
67 def _finalize_data(self, data, names):
68 final = {}
69 for d, f in names:
70 final[f] = data[d]
71 return final
72
73 def _postproc(self, request):
74 """Makes necessary modifications to user data and
75 sets up a stream.
76
77 :param request: request object
78 :type request: request
79 """
80 if request.status_code != 200:
81 raise Exception('wrong error code: {0}'.format(request.status_code))
82 else:
83 request = request.json()
84 if not len(request): raise errors.UserError('cannot extract user data: no posts to analyze')
85 names = [('id', 'id'),
86 ('diaspora_id', 'diaspora_id'),
87 ('guid', 'guid'),
88 ('name', 'diaspora_name'),
89 ('avatar', 'image_urls'),
90 ]
91 self.data = self._finalize_data(request[0]['author'], names)
92 self.stream = Outer(self._connection, location='people/{0}.json'.format(self['guid']))
93
94 def fetchhandle(self, protocol='https'):
95 """Fetch user data and posts using Diaspora handle.
96 """
97 pod, user = self._sephandle()
98 request = self._connection.session.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user))
99 self._postproc(request)
100
101 def fetchguid(self):
102 """Fetch user data and posts using guid.
103 """
104 request = self._connection.get('people/{0}.json'.format(self['guid']))
105 self._postproc(request)
106
107 def fetchprofile(self, protocol='https'):
108 """Fetch user data using Diaspora handle.
109 """
110 request = self._connection.get('people.json?q={0}'.format(self['handle']))
111 if request.status_code != 200:
112 raise Exception('wrong error code: {0}'.format(request.status_code))
113 else:
114 request = request.json()
115 if len(request):
116 names = [('id', 'id'),
117 ('handle', 'diaspora_id'),
118 ('guid', 'guid'),
119 ('name', 'diaspora_name'),
120 ('avatar', 'image_urls'),
121 ]
122 self.data = self._finalize_data(request[0], names)
123
124
125 class Contacts():
126 """This class represents user's list of contacts.
127 """
128 def __init__(self, connection):
129 self._connection = connection
130
131 def add(self, user_id, aspect_ids):
132 """Add user to aspects of given ids.
133
134 :param user_id: user guid
135 :type user_id: str
136 :param aspect_ids: list of aspect ids
137 :type aspect_ids: list
138 """
139 for aid in aspect_ids: Aspect(self._connection, aid).addUser(user_id)
140
141 def remove(self, user_id, aspect_ids):
142 """Remove user from aspects of given ids.
143
144 :param user_id: user guid
145 :type user_id: str
146 :param aspect_ids: list of aspect ids
147 :type aspect_ids: list
148 """
149 for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
150
151 def get(self, set=''):
152 """Returns list of user contacts.
153 Contact is a User() who is in one or more of user's
154 aspects.
155
156 By default, it will return list of users who are in
157 user's aspects.
158
159 If `set` is `all` it will also include users who only share
160 with logged user and are not in his/hers aspects.
161
162 If `set` is `only_sharing` it will return users who are only
163 sharing with logged user and ARE NOT in his/hers aspects.
164
165 :param set: if passed could be 'all' or 'only_sharing'
166 :type set: str
167 """
168 params = {}
169 if set: params['set'] = set
170
171 request = self._connection.get('contacts.json', params=params)
172 if request.status_code != 200:
173 raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
174 contacts = [User(self._connection, user['guid'], user['handle'], 'none', user['id']) for user in request.json()]
175 return contacts