Commit | Line | Data |
---|---|---|
c894b424 | 1 | import json |
d4a21d7e | 2 | import io |
41599bf2 | 3 | import mimetypes |
c894b424 | 4 | |
c64fc16b | 5 | from werkzeug.datastructures import FileStorage |
d4a21d7e | 6 | |
7 | from mediagoblin.media_types import sniff_media | |
d7b3805f | 8 | from mediagoblin.decorators import oauth_required |
967df5ef | 9 | from mediagoblin.federation.decorators import user_has_privilege |
c894b424 | 10 | from mediagoblin.db.models import User, MediaEntry, MediaComment |
a5682e89 | 11 | from mediagoblin.tools.response import redirect, json_response |
c894b424 | 12 | from mediagoblin.meddleware.csrf import csrf_exempt |
c64fc16b | 13 | from mediagoblin.submit.lib import new_upload_entry |
d7b3805f | 14 | |
247a3b78 | 15 | @oauth_required |
a5682e89 | 16 | def profile(request, raw=False): |
17 | """ This is /api/user/<username>/profile - This will give profile info """ | |
d7b3805f JT |
18 | user = request.matchdict["username"] |
19 | requested_user = User.query.filter_by(username=user) | |
c64fc16b | 20 | |
d7b3805f JT |
21 | # check if the user exists |
22 | if requested_user is None: | |
23 | error = "No such 'user' with id '{0}'".format(user) | |
24 | return json_response({"error": error}, status=404) | |
25 | ||
26 | user = requested_user[0] | |
27 | ||
a5682e89 | 28 | if raw: |
29 | return (user, user.serialize(request)) | |
30 | ||
d7b3805f JT |
31 | # user profiles are public so return information |
32 | return json_response(user.serialize(request)) | |
33 | ||
247a3b78 | 34 | @oauth_required |
a5682e89 | 35 | def user(request): |
36 | """ This is /api/user/<username> - This will get the user """ | |
37 | user, user_profile = profile(request, raw=True) | |
38 | data = { | |
39 | "nickname": user.username, | |
40 | "updated": user.created.isoformat(), | |
41 | "published": user.created.isoformat(), | |
42 | "profile": user_profile | |
43 | } | |
44 | ||
45 | return json_response(data) | |
46 | ||
247a3b78 | 47 | @oauth_required |
d4a21d7e | 48 | @csrf_exempt |
967df5ef | 49 | @user_has_privilege(u'uploader') |
d4a21d7e | 50 | def uploads(request): |
c64fc16b | 51 | """ Endpoint for file uploads """ |
d4a21d7e | 52 | user = request.matchdict["username"] |
53 | requested_user = User.query.filter_by(username=user) | |
54 | ||
55 | if requested_user is None: | |
56 | error = "No such 'user' with id '{0}'".format(user) | |
57 | return json_response({"error": error}, status=404) | |
58 | ||
59 | request.user = requested_user[0] | |
60 | if request.method == "POST": | |
61 | # Wrap the data in the werkzeug file wrapper | |
41599bf2 JT |
62 | mimetype = request.headers.get("Content-Type", "application/octal-stream") |
63 | filename = mimetypes.guess_all_extensions(mimetype) | |
64 | filename = 'unknown' + filename[0] if filename else filename | |
d4a21d7e | 65 | file_data = FileStorage( |
7810817c | 66 | stream=io.BytesIO(request.data), |
41599bf2 | 67 | filename=filename, |
6781ff3c | 68 | content_type=mimetype |
7810817c | 69 | ) |
70 | ||
71 | # Find media manager | |
41599bf2 | 72 | media_type, media_manager = sniff_media(file_data, filename) |
d4a21d7e | 73 | entry = new_upload_entry(request.user) |
7810817c | 74 | if hasattr(media_manager, "api_upload_request"): |
75 | return media_manager.api_upload_request(request, file_data, entry) | |
76 | else: | |
c3b89feb | 77 | return json_response({"error": "Not yet implemented"}, status=501) |
d4a21d7e | 78 | |
c3b89feb | 79 | return json_response({"error": "Not yet implemented"}, status=501) |
d4a21d7e | 80 | |
247a3b78 | 81 | @oauth_required |
c894b424 | 82 | @csrf_exempt |
d7b3805f JT |
83 | def feed(request): |
84 | """ Handles the user's outbox - /api/user/<username>/feed """ | |
85 | user = request.matchdict["username"] | |
86 | requested_user = User.query.filter_by(username=user) | |
87 | ||
88 | # check if the user exists | |
89 | if requested_user is None: | |
90 | error = "No such 'user' with id '{0}'".format(user) | |
91 | return json_response({"error": error}, status=404) | |
92 | ||
3c3fa5e7 | 93 | request.user = requested_user[0] |
6781ff3c | 94 | if request.data: |
c894b424 | 95 | data = json.loads(request.data) |
6781ff3c JT |
96 | else: |
97 | data = {"verb": None, "object": {}} | |
98 | ||
99 | if request.method == "POST" and data["verb"] == "post": | |
c894b424 JT |
100 | obj = data.get("object", None) |
101 | if obj is None: | |
102 | error = {"error": "Could not find 'object' element."} | |
103 | return json_response(error, status=400) | |
c64fc16b | 104 | |
c894b424 JT |
105 | if obj.get("objectType", None) == "comment": |
106 | # post a comment | |
107 | media = int(data["object"]["inReplyTo"]["id"]) | |
c894b424 JT |
108 | comment = MediaComment( |
109 | media_entry=media, | |
110 | author=request.user.id, | |
111 | content=data["object"]["content"] | |
112 | ) | |
113 | comment.save() | |
3c3fa5e7 | 114 | data = {"verb": "post", "object": comment.serialize(request)} |
115 | return json_response(data) | |
7810817c | 116 | |
62dc7d3e | 117 | elif obj.get("objectType", None) == "image": |
118 | # Posting an image to the feed | |
119 | # NB: This is currently just handing the image back until we have an | |
120 | # to send the image to the actual feed | |
c64fc16b | 121 | |
62dc7d3e | 122 | media_id = int(data["object"]["id"]) |
123 | media = MediaEntry.query.filter_by(id=media_id) | |
124 | if media is None: | |
125 | error = "No such 'image' with id '{0}'".format(id=media_id) | |
126 | return json_response(error, status=404) | |
127 | media = media[0] | |
c3b89feb JT |
128 | return json_response({ |
129 | "verb": "post", | |
130 | "object": media.serialize(request) | |
131 | }) | |
62dc7d3e | 132 | |
c894b424 | 133 | elif obj.get("objectType", None) is None: |
62dc7d3e | 134 | # They need to tell us what type of object they're giving us. |
c894b424 JT |
135 | error = {"error": "No objectType specified."} |
136 | return json_response(error, status=400) | |
137 | else: | |
62dc7d3e | 138 | # Oh no! We don't know about this type of object (yet) |
c64fc16b | 139 | error_message = "Unknown object type '{0}'.".format( |
140 | obj.get("objectType", None) | |
141 | ) | |
142 | ||
143 | error = {"error": error_message} | |
c894b424 JT |
144 | return json_response(error, status=400) |
145 | ||
6781ff3c JT |
146 | elif request.method in ["PUT", "POST"] and data["verb"] == "update": |
147 | # Check we've got a valid object | |
148 | obj = data.get("object", None) | |
149 | ||
150 | if obj is None: | |
151 | error = {"error": "Could not find 'object' element."} | |
152 | return json_response(error, status=400) | |
153 | ||
154 | if "objectType" not in obj: | |
155 | error = {"error": "No objectType specified."} | |
156 | return json_response(error, status=400) | |
157 | ||
158 | if "id" not in obj: | |
159 | error = {"error": "Object ID has not been specified."} | |
160 | return json_response(error, status=400) | |
161 | ||
162 | obj_id = obj["id"] | |
163 | ||
164 | # Now try and find object | |
165 | if obj["objectType"] == "comment": | |
166 | comment = MediaComment.query.filter_by(id=obj_id) | |
167 | if comment is None: | |
168 | error = {"error": "No such 'comment' with id '{0}'.".format(obj_id)} | |
169 | return json_response(error, status=400) | |
170 | comment = comment[0] | |
171 | ||
172 | # TODO: refactor this out to update/setting method on MediaComment | |
173 | if obj.get("content", None) is not None: | |
174 | comment.content = obj["content"] | |
175 | ||
176 | comment.save() | |
177 | activity = { | |
178 | "verb": "update", | |
179 | "object": comment.serialize(request), | |
180 | } | |
181 | return json_response(activity) | |
182 | ||
183 | elif obj["objectType"] == "image": | |
184 | image = MediaEntry.query.filter_by(id=obj_id) | |
185 | if image is None: | |
186 | error = {"error": "No such 'image' with the id '{0}'.".format(obj_id)} | |
187 | return json_response(error, status=400) | |
188 | ||
189 | image = image[0] | |
190 | ||
191 | # TODO: refactor this out to update/setting method on MediaEntry | |
192 | if obj.get("displayName", None) is not None: | |
193 | image.title = obj["displayName"] | |
194 | ||
195 | if obj.get("content", None) is not None: | |
196 | image.description = obj["content"] | |
197 | ||
198 | if obj.get("license", None) is not None: | |
199 | # I think we might need some validation here | |
200 | image.license = obj["license"] | |
201 | ||
202 | image.save() | |
203 | activity = { | |
204 | "verb": "update", | |
205 | "object": image.serialize(request), | |
206 | } | |
207 | return json_response(activity) | |
7810817c | 208 | |
c894b424 | 209 | feed_url = request.urlgen( |
6781ff3c JT |
210 | "mediagoblin.federation.feed", |
211 | username=request.user.username, | |
212 | qualified=True | |
213 | ) | |
c894b424 JT |
214 | |
215 | feed = { | |
c64fc16b | 216 | "displayName": "Activities by {user}@{host}".format( |
217 | user=request.user.username, | |
218 | host=request.host | |
219 | ), | |
c894b424 JT |
220 | "objectTypes": ["activity"], |
221 | "url": feed_url, | |
222 | "links": { | |
223 | "first": { | |
224 | "href": feed_url, | |
225 | }, | |
226 | "self": { | |
227 | "href": request.url, | |
228 | }, | |
229 | "prev": { | |
230 | "href": feed_url, | |
231 | }, | |
232 | "next": { | |
233 | "href": feed_url, | |
234 | } | |
235 | }, | |
3c3fa5e7 | 236 | "author": request.user.serialize(request), |
c894b424 JT |
237 | "items": [], |
238 | } | |
c64fc16b | 239 | |
d7b3805f JT |
240 | |
241 | # Now lookup the user's feed. | |
c894b424 JT |
242 | for media in MediaEntry.query.all(): |
243 | feed["items"].append({ | |
244 | "verb": "post", | |
245 | "object": media.serialize(request), | |
3c3fa5e7 | 246 | "actor": request.user.serialize(request), |
247 | "content": "{0} posted a picture".format(request.user.username), | |
c894b424 JT |
248 | "id": 1, |
249 | }) | |
250 | feed["items"][-1]["updated"] = feed["items"][-1]["object"]["updated"] | |
251 | feed["items"][-1]["published"] = feed["items"][-1]["object"]["published"] | |
252 | feed["items"][-1]["url"] = feed["items"][-1]["object"]["url"] | |
253 | feed["totalItems"] = len(feed["items"]) | |
254 | ||
255 | return json_response(feed) | |
d7b3805f JT |
256 | |
257 | @oauth_required | |
98596dd0 | 258 | def object(request, raw_obj=False): |
5a2056f7 | 259 | """ Lookup for a object type """ |
6781ff3c | 260 | object_type = request.matchdict["objectType"] |
5a2056f7 | 261 | uuid = request.matchdict["uuid"] |
6781ff3c JT |
262 | if object_type not in ["image"]: |
263 | error = "Unknown type: {0}".format(object_type) | |
c64fc16b | 264 | # not sure why this is 404, maybe ask evan. Maybe 400? |
5a2056f7 JT |
265 | return json_response({"error": error}, status=404) |
266 | ||
d461fbe5 | 267 | media = MediaEntry.query.filter_by(slug=uuid).first() |
5a2056f7 JT |
268 | if media is None: |
269 | # no media found with that uuid | |
6781ff3c | 270 | error = "Can't find a {0} with ID = {1}".format(object_type, uuid) |
5a2056f7 JT |
271 | return json_response({"error": error}, status=404) |
272 | ||
98596dd0 | 273 | if raw_obj: |
274 | return media | |
275 | ||
bdde87a4 | 276 | return json_response(media.serialize(request)) |
98596dd0 | 277 | |
247a3b78 | 278 | @oauth_required |
98596dd0 | 279 | def object_comments(request): |
280 | """ Looks up for the comments on a object """ | |
281 | media = object(request, raw_obj=True) | |
282 | response = media | |
283 | if isinstance(response, MediaEntry): | |
284 | comments = response.serialize(request) | |
285 | comments = comments.get("replies", { | |
6781ff3c JT |
286 | "totalItems": 0, |
287 | "items": [], | |
288 | "url": request.urlgen( | |
289 | "mediagoblin.federation.object.comments", | |
290 | objectType=media.objectType, | |
291 | uuid=media.slug, | |
292 | qualified=True | |
293 | ) | |
294 | }) | |
295 | ||
c894b424 JT |
296 | comments["displayName"] = "Replies to {0}".format(comments["url"]) |
297 | comments["links"] = { | |
298 | "first": comments["url"], | |
299 | "self": comments["url"], | |
300 | } | |
98596dd0 | 301 | response = json_response(comments) |
302 | ||
303 | return response | |
a5682e89 | 304 | |
305 | ||
306 | ## | |
307 | # Well known | |
308 | ## | |
309 | def host_meta(request): | |
310 | """ This is /.well-known/host-meta - provides URL's to resources on server """ | |
311 | links = [] | |
c64fc16b | 312 | |
a5682e89 | 313 | # Client registration links |
314 | links.append({ | |
315 | "ref": "registration_endpoint", | |
316 | "href": request.urlgen("mediagoblin.oauth.client_register", qualified=True), | |
317 | }) | |
318 | links.append({ | |
319 | "ref": "http://apinamespace.org/oauth/request_token", | |
320 | "href": request.urlgen("mediagoblin.oauth.request_token", qualified=True), | |
321 | }) | |
322 | links.append({ | |
323 | "ref": "http://apinamespace.org/oauth/authorize", | |
324 | "href": request.urlgen("mediagoblin.oauth.authorize", qualified=True), | |
325 | }) | |
326 | links.append({ | |
327 | "ref": "http://apinamespace.org/oauth/access_token", | |
328 | "href": request.urlgen("mediagoblin.oauth.access_token", qualified=True), | |
329 | }) | |
330 | ||
18297655 | 331 | return json_response({"links": links}) |
a5682e89 | 332 | |
333 | def whoami(request): | |
334 | """ This is /api/whoami - This is a HTTP redirect to api profile """ | |
335 | profile = request.urlgen( | |
336 | "mediagoblin.federation.user.profile", | |
337 | username=request.user.username, | |
338 | qualified=True | |
339 | ) | |
340 | ||
341 | return redirect(request, location=profile) |