def new_group(request, group_name, group_description): name = group_name description = group_description if models.Category.all_objects.get_or_none(name=name): raise ServiceError('A group already exists with that name.') if models.Category.objects.filter( founder=request.user).count() >= models.Category.FOUND_LIMIT: raise ServiceError( 'Sorry, you can only found up to %s groups at this time.' % models.Category.FOUND_LIMIT) group = models.Category( founder=request.user, founded=time.time(), name=name, description=description, ) problem = group.validate() if problem: raise ServiceError(problem) group.save() request.user.userinfo.details.force() # A founder auto-follows their group. models.FollowCategory(category=group, user=request.user).save() group_info = {'name': name, 'description': description} fact.record('new_group', request, group_info) Metrics.new_group.record(request, group_info=group_info)
def complete_quest(user, quest_comment, access_token, request=None): if user.id != quest_comment.author_id: raise ServiceError("You can't share to your timeline a drawing you didn't create.") try: user.facebookuser except FacebookUser.DoesNotExist: raise ServiceError("Can't share to your timeline if you haven't added your Facebook account yet.") # Although we've renamed it to "draw", Facebook still internally refers to it as "complete". action = 'complete' quest_url = quest_comment.get_share_page_url_with_tracking(user, 'facebook', absolute=True) @bgwork.defer def rewards(): economy.credit_personal_share(user) send_action = '{}:{}'.format(settings.FACEBOOK_NAMESPACE, action) @bgwork.defer def do_graph_action(): try: graph = GraphAPI(access_token) graph.put_object('me', send_action, quest=quest_url) if request: Metrics.share_to_timeline.record(request, quest=quest_url) except GraphAPIError as e: if request: Metrics.share_to_timeline_error.record(request, quest=quest_url) client.create_from_exception()
def set_playback_data(request): comment = None if 'uuid' not in request.POST: try: comment_id = request.POST['comment_id'] except KeyError: raise ServiceError("Missing comment ID.") comment = get_object_or_404(QuestComment, id=comment_id) if request.user.id != comment.author.id: raise ServiceError("Can't upload playback data to a drawing you didn't create.") if 'playback_plist_data' in request.FILES: #plist = readPlistFromBytes(b''.join(request.FILES.get('playback_plist_data').chunks())) plist = readPlist(request.FILES['playback_plist_data']) playback_data = json.backend_dumps(plist) elif 'playback_data' in request.FILES: playback_data = u''.join(request.FILES.get('playback_data').chunks()) else: playback_data = request.POST['playback_data'] if comment is not None: save_playback_data(playback_data, comment=comment) else: save_playback_data(playback_data, uuid=request.POST['uuid'])
def combine_upload_chunks(request, chunks, metadata, is_quest=False): keys = ['chunk:{0}'.format(chunk) for chunk in chunks] raw_values = redis.mget(keys) if not all(raw_values): raise ServiceError("Missing uploaded chunk, please retry.") values = [b64decode(val) for val in raw_values] filedata = "".join(values) fs = get_fs(*settings.IMAGE_FS) remix_of = metadata.get('remix_of') stamps_used = metadata.get('used_stamps', []) or [] text_used = metadata.get('used_text', '') or '' redis.delete(*keys) try: return create_content(request.META['REMOTE_ADDR'], fs, filedata, remix_of, stamps_used, text_used, '', is_quest=is_quest) except IOError, e: raise ServiceError("Unable to read image.")
def invite(self, inviter, invitee, type='invite'): if invitee.id in self: raise ServiceError("User has already been invited.") if invitee.id == inviter.id: raise ServiceError( "You can't invite yourself to remix! You're already here!") if self.comment.author == invitee: raise ServiceError("That user is already in this thread.") self.actions[type](inviter, self.comment, invitee) self.sadd(invitee.id) invitee.remix_invites.add_invite(self.comment)
def add_sticker_to_comment(request, comment_id, type_id, epic_message=None): """ Stickers a comment. You can be logged in or out. """ sticker = stickers.get(int(type_id)) comment = get_object_or_404(models.Comment, pk=comment_id) if epic_message and len(epic_message) > knobs.STICKER_MESSAGE_MAX_LENGTH: raise ServiceError("Message is too long.") elif epic_message and not (sticker.cost and sticker.cost >= knobs.EPIC_STICKER_COST_THRESHOLD): raise ServiceError("Messages can only be attached to epic stickers.") # Calculate if this user has exceeded the stickering rate limit. prefix = 'user:%s:stick_limit:' % request.user.id if not RateLimit(prefix + 'h', 600, 60 * 60).allowed() or not RateLimit( prefix + 'd', 1000, 8 * 60 * 60).allowed(): Metrics.sticker_ratelimit.record(request, sticker_type=sticker.type_id, comment=comment.id) raise ServiceError("Attempting to sticker too quickly.") prev_top_sticker = comment.details().top_sticker() remaining = _sticker_comment(request, comment, sticker.type_id, epic_message=epic_message) Metrics.sticker.record(request, sticker_type=sticker.type_id, comment=comment.id) comment_details = comment.details() top_sticker = comment_details.top_sticker() @bgwork.defer def update_stickered_users(): get_most_stickered_unfollowed_users(request.user).force() if prev_top_sticker is None or prev_top_sticker['type_id'] != top_sticker: @bgwork.defer def update_footer(): if comment.footer.should_exist(): comment.footer.call_update_in_new_process() return { 'new_counts': comment_details.sticker_counts, 'sorted_counts': comment_details.sorted_sticker_counts(), 'top_sticker': top_sticker, 'remaining': remaining, }
def verify_first_party_url(url): """ Also allows iTunes store URLs. """ if not url or not url.startswith('/'): parsed_url = urlparse.urlparse(url) try: protocol = parsed_url[0] domain = parsed_url[1] except IndexError: raise ServiceError("Invalid share url.") if protocol not in ['http', 'https'] or domain not in ['itunes.apple.com', 'example.com']: # Only 1st party redirects, to avoid security holes that 3rd party redirects imply raise ServiceError("Invalid share url.")
def groups(request, payload={}, group_name=None): """ Groups endpoint of the example.com public api Request with an id parameter: /public_api/groups/funny POST JSON in the following format: POST /public_api/groups/ {"ids":["funny","canvas"]} Group posts will be returned """ + str(knobs.PUBLIC_API_PAGINATION_SIZE) + """ at a time, ordered newest to oldest. You can request posts beyond the initial range by POSTing JSON in the following format: POST /public_api/groups/ {"ids":[{"group":"funny","skip":100},"canvas"} """ Metrics.api_group.record(request) ids = payload.get('ids') if group_name and not ids: try: return PublicAPIGroupDetails(group_name).to_client() except (ObjectDoesNotExist, Http404): raise ServiceError("Group does not exist") elif ids: def inner_group(group_arg): try: return PublicAPIGroupDetails(group_arg).to_client() except (ObjectDoesNotExist, Http404): return None potential_groups = [inner_group(x) for x in payload.get('ids')] return {'groups': [x for x in potential_groups if x]}
def users(request, payload={}, username=None): """ Users endpoint of the example.com public api Request with an id parameter: /public_api/users/watermelonbot POST JSON in the following format: POST /public_api/users/ {"ids":["watermelonbot", "jeff"]} User posts will be returned """ + str(knobs.PUBLIC_API_PAGINATION_SIZE) + """ at a time, ordered newest to oldest. You can request posts beyond the initial range by POSTing JSON in the following format: POST /public_api/users/ {"ids":[{"user":"******","skip":100},"jeff"} """ Metrics.api_user.record(request) if username and not payload: try: return PublicAPIUserDetails(username).to_client() except (ObjectDoesNotExist, Http404): raise ServiceError("User does not exist") elif payload: def inner_user(user_arg): try: return PublicAPIUserDetails(user_arg).to_client() except (ObjectDoesNotExist, Http404): return None potential_users = [inner_user(x) for x in payload.get('ids')] return {'users': [x for x in potential_users if x]}
def posts(request, payload={}, short_id=None): """ Posts endpoint of the example.com public api Request with an id parameter: /public_api/posts/1qkx8 POST JSON in the following format: POST /public_api/posts/ {"ids":["1qkx8","ma6fz"]} """ Metrics.api_comment.record(request) ids = payload.get('ids') if short_id and not ids: try: comment = Comment.details_by_id(long_id(short_id), promoter=PublicAPICommentDetails) (comment,) = CachedCall.multicall([comment]) return comment.to_client() except (ObjectDoesNotExist, util.Base36DecodeException): raise ServiceError("Post not found") elif ids: ids = [long_id(x) for x in set(ids)] calls = [Comment.details_by_id(id, ignore_not_found=True, promoter=PublicAPICommentDetails) for id in ids] comments = CachedCall.multicall(calls, skip_decorator=True) return {'posts': [x.to_client() for x in comments if x]}
def api_upload(request): content_type = request.META['CONTENT_TYPE'] url = '' filedata = None if 'file' in request.FILES: filedata = "".join(request.FILES['file'].chunks()) elif content_type.startswith('application/json'): json = util.loads(request.raw_post_data) url = json.get('url', '').strip() filedata = _fetch_url(url) elif content_type.startswith('application/base64-png'): token = 'data:image/png;base64,' header, data = request.raw_post_data.split(',', 2) if not header.startswith( 'data:' ) or not 'image/png' in header or not 'base64' in header: return { 'success': False, 'code': 'unknown', 'reason': 'Missing data url header.' } else: filedata = b64decode(data) if filedata: return _got_imagedata(filedata, request, url=url) else: raise ServiceError("No file or url.")
def search_stamps(request, query, start): """ Searches the special "stamps" group for stamps that match the search query. Returns {comments: [list of comment details]} """ qs = query try: start = int(start) except TypeError: raise ServiceError('Invalid "start" parameter.') stamps = models.Category.objects.get(name="stamps") if qs: ids = [ x for x in models.Comment.objects.filter( category=stamps).filter(Q( title__icontains=qs)).values_list('id', flat=True) ] ids = ids[start:start + 32] comments = models.Comment.curated.exclude( reply_content__id=None).in_bulk(ids) details = CachedCall.multicall( [comments[id].details for id in ids if id in comments]) else: comments = models.Comment.curated.filter(category=stamps).exclude( reply_content__id=None).order_by('-id') comments = comments[start:start + 32] details = CachedCall.queryset_details(comments) return {'comments': details}
def api_upload(request): content_type = request.META.get('CONTENT_TYPE', '') url = '' filedata = None if 'file' in request.FILES: filedata = "".join(request.FILES['file'].chunks()) elif content_type.startswith('application/json'): json = util.loads(request.body) url = json.get('url', '').strip() filedata = _fetch_url(url) elif content_type.startswith('application/base64-png'): token = 'data:image/png;base64,' header, data = request.body.split(',', 2) if not header.startswith('data:') or not 'image/png' in header or not 'base64' in header: return {'success': False, 'code': 'unknown', 'reason': 'Missing data url header.'} else: filedata = b64decode(data) if filedata: ret = _got_imagedata(filedata, request, url=url) from canvas.cache_patterns import cache from canvas.models import Content util.papertrail.debug('UPLOADS: _got_imagedata, actual cache is {} for content ID {}'.format(cache.get('content:%s:full_details_v26' % ret['content']['id']), ret['content']['id'])) util.papertrail.debug('UPLOADS: _got_imagedata, actual content object for ID {} exists: {}'.format(ret['content']['id'], Content.all_objects.filter(id=ret['content']['id']).exists())) util.papertrail.debug('UPLOADS: _got_imagedata: {} {}'.format(ret.get('success'), ret['content']['id'])) return ret else: raise ServiceError("No file or url.")
def update_comment_tags(request, comment_id, tags): comment = get_object_or_404(models.Comment, pk=comment_id) if request.user.is_staff or request.user == comment.author: new_tags = set(tags) original = comment.tags.smembers() # tags to remove for t in (original - new_tags): Tag(t).untag_comment(comment) comment.tags.srem(t) # tags to add for t in (new_tags - original): Tag(t).tag_comment(comment) comment.tags.sadd(t) comment.details.force() ret_tags = [] for t in comment.tags.smembers(): ret_tags += [{ 'name': t, 'url': Tag(t).get_absolute_url(), }] return {'tags': ret_tags} else: raise ServiceError("Permission denied")
def flag_comment(request, comment_id): comment = get_object_or_404(models.Comment, pk=comment_id) # If the user hits the flag rate limit, silently ignore it. prefix = 'user:%s:flag_limit:' % request.user.id def allowed(key, val): if request.user.is_staff: return True freq, timespan = val return RateLimit(prefix+key, freq, timespan).allowed() if not all(allowed(key, val) for key,val in knobs.FLAG_RATE_LIMITS.iteritems()): Metrics.flag_ratelimit.record(request, comment=comment.id) raise ServiceError('Flag rate limit exceeded.') else: flag = comment.add_flag(request.user, ip=request.META['REMOTE_ADDR']) if flag is not None: if settings.PROJECT == 'canvas': request.user.redis.hidden_comments.hide_comment(comment) Metrics.flag.record(request, comment=comment.id) if comment.anonymous: Metrics.flag_anonymous_post.record(request, comment=comment.id) if settings.AUTO_MODERATE_FLAGGED_COMMENTS_THRESHOLD is not None: if (not comment.judged and comment.details().flag_counts[0] == settings.AUTO_MODERATE_FLAGGED_COMMENTS_THRESHOLD): comment.visibility = models.Visibility.DISABLED comment.save() comment.visibility_changed() return {'flag_counts': comment.details().flag_counts, 'flag_id': flag.id}
def associate_facebook_account(user, facebook_access_token, request=None): try: fb_user = FacebookUser.get_or_create_from_access_token( facebook_access_token, request=request) except GraphAPIError as e: papertrail.debug( u'GraphAPIError inside associate_facebook_account: {} (user: {}, token: {})' .format(e.message, user.username, facebook_access_token)) raise ServiceError( _("There appears to be an issue communicating with Facebook. Please try again." )) try: existing_fb_user = user.facebookuser if existing_fb_user.fb_uid == fb_user.fb_uid: return existing_fb_user.user = None existing_fb_user.save() except FacebookUser.DoesNotExist: pass fb_user.user = user fb_user.save() @bgwork.defer def notify_friends(): fb_user.notify_friends_of_signup(facebook_access_token) fb_user.respond_to_apprequest_invites(facebook_access_token)
def tumblr_post_photo(request, access_token, access_token_secret, blog_hostname, comment_id): comment = get_object_or_404(QuestComment, id=comment_id) try: models.post_photo(request.user, blog_hostname, comment) except requests.exceptions.HTTPError as e: client.create_from_exception() raise ServiceError("Error posting to Tumblr.")
def JSONPResponse(request, response, **kwargs): callback = request.GET.get('callback', 'callback') if not callback.replace('_', '').isalnum(): raise ServiceError() return HttpResponse( '%s(%s);' % (callback, client_dumps(response, viewer=request.user)), mimetype='application/javascript', **kwargs)
def claim_comment(request, comment_id): comment = models.Comment.all_objects.get(id=comment_id) if not comment.author == request.user: raise ServiceError("Not comment author") comment.make_non_anonymous(request.user) Metrics.claim_post.record(request, comment=comment.id)
def check_canvas_account_password(self, username, password): try: ret = _canvas_api('/user/check_password', {'username': username, 'password': password}) except (urllib2.URLError, socket.timeout, httplib.BadStatusLine,): raise ServiceError("Couldn't migrate Canvas account to DrawQuest. Please try again later.") if not ret['success']: raise Exception("Couldn't migrate Canvas account to DrawQuest.") del ret['success'] return ret['correct']
def _fetch_url(url): if not url.startswith('http://') and not url.startswith('https://'): if '://' not in url: url = 'http://' + url else: # Must at least prevent file://, better to whitelist than blacklist raise ServiceError("Only http/https links allowed") try: url_request = urllib2.Request(url) url_response = urllib2.urlopen(url_request) except (IOError, httplib.HTTPException, UnicodeEncodeError): raise ServiceError("Unable to download image.") if url_response.getcode() != 200: raise ServiceError("The requested image could not be downloaded. Please try a different image.") else: return url_response.read()
def staff_gift_coins(request, username, amount): amount = int(amount) if amount > 400: raise ServiceError( "Can't gift more than 400 at a time (sanity check).") user = get_object_or_404(User, username=username) economy.credit(user, amount)
def script_share(request, s3sum): if not s3sum.isalnum(): raise ServiceError('sums must be alphanumeric') remixplugin = models.RemixPlugin(author=request.user, timestamp=Now(), s3md5=s3sum) remixplugin.save() return {'plugin_url': remixplugin.get_url()}
def canvas_account_info(self, username): try: ret = _canvas_api('/user/info_for_drawquest_migration', {'username': username}) except (urllib2.URLError, socket.timeout, httplib.BadStatusLine,): raise ServiceError("Couldn't migrate Canvas account to DrawQuest. Please try again later.") if not ret['success']: raise Exception("Couldn't migrate Canvas account to DrawQuest.") del ret['success'] return ret
def delete_comment(request, comment_id): comment = models.Comment.all_objects.get(id=comment_id) if not comment.author == request.user: raise ServiceError("Not comment author") comment.moderate_and_save(models.Visibility.UNPUBLISHED, request.user, undoing=True) Metrics.delete_post.record(request, comment=comment.id)
def invite(self, inviter, invitees, type='invite', ignore_errors=False): if not ignore_errors: for invitee in invitees: if invitee.id in self: raise ServiceError( "User {} has already been invited.".format( invitee.username)) if invitee.id == inviter.id: raise ServiceError( "You can't invite yourself - you're already here!") if self.quest.author == invitee: raise ServiceError("That user is already in this quest.") self.sadd([invitee.id for invitee in invitees]) for invitee in invitees: Actions.invite_user(inviter, self.quest, invitee) invitee.redis.quest_invites.add_invite(self.quest)
def post_quest_comment(request, quest_id, content_id, fact_metadata={}, facebook_share=False, facebook_access_token=None): # Rate-limit? if not request.user.is_staff: prefix = 'user:{}:post_limit:'.format(request.user.id) if not RateLimit(prefix + 'h', 60, 60 * 60).allowed() or not RateLimit( prefix + 'd', 100, 8 * 60 * 60).allowed(): raise ServiceError("Attempting to post drawings too quickly.") _, parent_comment, content, _, _, _ = validate_and_clean_comment( request.user, parent_comment=quest_id, reply_content=content_id, ) if facebook_share: if not facebook_access_token: raise ServiceError( "Can't share to your timeline if you haven't signed into Facebook yet." ) associate_facebook_account(request.user, facebook_access_token) comment = QuestComment.create_and_post(request, request.user, content, parent_comment, fact_metadata=fact_metadata) if facebook_share: complete_quest(request.user, comment, facebook_access_token, request=request) return { 'comment': comment.details(), 'balance': economy.balance(request.user), }
def metric_record(request, name, info={}): info = dict((str(key), value) for (key, value) in info.items()) metric = Metrics.all.get(name) if not metric: raise ServiceError("Invalid metric name") if metric.ignore_from_api: return metric.record(request, **info)
def set_playback_data(request): try: comment_id = request.POST['comment_id'] except KeyError: raise ServiceError("Missing comment ID.") comment = get_object_or_404(QuestComment, id=comment_id) playback_data = request.POST['playback_data'] PlaybackData.create_with_json(comment, playback_data)
def check_star_rate_limit(request, comment): from drawquest.apps.stars.models import get_star_sticker # Calculate if this user has exceeded the stickering rate limit. prefix = 'user:{}:stick_limit:'.format(request.user.id) if not RateLimit(prefix + 'h', 500, 60 * 60).allowed() or not RateLimit( prefix + 'd', 1000, 8 * 60 * 60).allowed(): Metrics.sticker_ratelimit.record( request, sticker_type=get_star_sticker().type_id, comment=comment.id) raise ServiceError("Attempting to star too quickly.")
def __init__(self): ServiceError.__init__(self, "No such notification.")