From fa48a9588477059c4c2c46e6f15be75d7a5e3cb9 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Thu, 2 Sep 2010 16:29:21 +1000 Subject: [PATCH] first cut at send --- linkdrop/controllers/send.py | 124 ++++++++++++++++++++++++++++++ linkdrop/model/history.py | 8 +- web/scratch/send/index.html | 145 +++++++++++++++++++++++++++++++++++ web/scratch/send/index.js | 109 ++++++++++++++++++++++++++ 4 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 linkdrop/controllers/send.py create mode 100644 web/scratch/send/index.html create mode 100644 web/scratch/send/index.js diff --git a/linkdrop/controllers/send.py b/linkdrop/controllers/send.py new file mode 100644 index 0000000..8df7f7b --- /dev/null +++ b/linkdrop/controllers/send.py @@ -0,0 +1,124 @@ +import logging +import datetime +import json +import urllib + +from pylons import config, request, response, session +from pylons.controllers.util import abort, redirect +from pylons.decorators.util import get_pylons + +from linkdrop.lib.base import BaseController +from linkdrop.lib.helpers import json_exception_response, api_response, api_entry, api_arg +from linkdrop.lib.oauth.base import get_oauth_config + +from linkdrop.model.meta import Session +from linkdrop.model import Account, History +from linkdrop.model.types import UTCDateTime +from sqlalchemy.orm.exc import NoResultFound + +log = logging.getLogger(__name__) + + +class SendController(BaseController): + """ +Send +==== + +The 'send' namespace is used to send updates to our supported services. + +""" + __api_controller__ = True # for docs + + def _handle_auth_failure(self, reason): + try: + redirect(request.POST['end_point_auth_failure']) + except KeyError: + abort(401, reason) + + @api_response + @json_exception_response + def send(self): + # If we don't have a userkey in our session we bail early with a + # 401 + userkey = session.get('userkey') + if not userkey: + self._handle_auth_failure('no session data') + try: + domain = request.params.get('domain') + message = request.params['message'] + except KeyError, what: + raise ValueError("'%s' request param is not optional" % (what,)) + + # even if we have a session key, we must have an account for that + # user for the specified domain. + try: + acct = Session.query(Account).filter_by(userkey=userkey, domain=domain).one() + except NoResultFound: + self._handle_auth_failure("no account for that domain") + + result = {} + error = None + # send the item. + if domain=="twitter.com": + from twitter.oauth import OAuth + from twitter.api import Twitter, TwitterHTTPError + oauth_config = get_oauth_config(domain) + auth = OAuth(token=acct.oauth_token, + token_secret=acct.oauth_token_secret, + consumer_key=oauth_config['consumer_key'], + consumer_secret=oauth_config['consumer_secret']) + try: + api = Twitter(auth=auth) + status = api.statuses.update(status=message) + result[domain] = status['id'] + except TwitterHTTPError, exc: + details = json.load(exc.e) + if 'error' in details: + msg = details['error'] + else: + msg = str(details) + error = {'provider': domain, + 'reason': msg, + } + elif domain=="facebook.com": + url = "https://graph.facebook.com/me/feed" + body = urllib.urlencode({"message": message}) + response = json.load(urllib.urlopen(url + + urllib.urlencode(dict(access_token=acct.oauth_token)), body)) + if 'id' in response: + result[domain] = response['id'] + elif 'error' in response: + if response['error'].get('type')=="OAuthInvalidRequestException": + abort(401, "oauth token was rejected (%s)" % (response['error'],)) + error = {'provider': domain, + 'reason': response['error'], + } + else: + log.error("unexpected facebook response: %r", response) + else: + raise ValueError, "unsupported service %r" % (domain,) + + if error: + assert not result + log.info("send failure: %r", error) + try: + redirect(request.POST['end_point_failure']) + except KeyError: + pass + else: + # create a new record in the history table. + assert result + history = History() + history.account = acct + history.published = UTCDateTime.now() + history.message = message + Session.add(history) + Session.commit() + result['linkdrop'] = history.id + log.info("send success - linkdrop id is %s", history.id) + try: + redirect(request.POST['end_point_success']) + except KeyError: + pass + # no redirects requests, just return the response. + return {'result': result, 'error': error} diff --git a/linkdrop/model/history.py b/linkdrop/model/history.py index 0dc497e..3eff5d2 100644 --- a/linkdrop/model/history.py +++ b/linkdrop/model/history.py @@ -14,11 +14,7 @@ class History(JsonExpandoMixin, SerializerMixin, Base): __table_args__ = make_table_args() id = Column(Integer, primary_key=True) - userkey = Column(Integer, ForeignKey(Account.userkey), index=True) + account_id = Column(None, ForeignKey(Account.id), index=True) published = Column(UTCDateTime, nullable=False) - # svckey is from the account table, a key to domain/userid/username, - # it might end up being a relationship - svckey = Column(RDUnicode(128), nullable=False) - - account = relationship('Account', uselist=False) + account = relationship('Account') diff --git a/web/scratch/send/index.html b/web/scratch/send/index.html new file mode 100644 index 0000000..05017c4 --- /dev/null +++ b/web/scratch/send/index.html @@ -0,0 +1,145 @@ + + + + + + + Send a message + + + + + + + + +
+ +
+ + +
+
+
+
+ + Send via + Twitter + Facebook + + + + + + +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/web/scratch/send/index.js b/web/scratch/send/index.js new file mode 100644 index 0000000..14501d3 --- /dev/null +++ b/web/scratch/send/index.js @@ -0,0 +1,109 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Raindrop. + * + * The Initial Developer of the Original Code is + * Mozilla Messaging, Inc.. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * */ + +/*jslint plusplus: false */ +/*global require: false, location: true, window: false, alert: false */ +"use strict"; + +require.def("send", + ["require", "jquery", "blade/fn", "rdapi", "placeholder", "blade/url"], +function (require, $, fn, rdapi, placeholder, url) { + + var validHashRegExp = /^\w+$/; + + function onHashChange() { + var value = location.hash.split("#")[1], + start, end; + + value = value || "send"; + + if (validHashRegExp.test(value)) { + $(".section").each(function (i, node) { + node = $(node); + if (node.hasClass(value)) { + end = node; + } else if (!node.hasClass("hidden")) { + start = node; + } + }); + } + if (value==="auth_failure") { + document.authForm.domain.value = localStorage['X-Send-Domain'] + document.authForm.submit() + console.log("submitted auth form...") + } else if (value==="auth_success") { + debugger; + var radio = document.messageForm.domain; + for(var i = 0; i < radio.length; i++) { + if(radio[i].value==localStorage['X-Send-Domain']) { + radio[i].checked = true; + } + } + document.messageForm.message.value = localStorage['X-Send-Domain']; + document.messageForm.submit() + } + + //Animate! + if (start) start.addClass("hidden"); + if (end) end.removeClass("hidden") + } + + //Set up hashchange listener + window.addEventListener("hashchange", onHashChange, false); + + $(function () { + + $("#messageForm") + .submit(function (evt) { + //First clear old errors + $(".error").addClass("invisible"); + + var form = evt.target; + + //Make sure all form elements are trimmed and username exists. + $.each(form.elements, function (i, node) { + var trimmed = node.value.trim(); + + if (node.getAttribute("placeholder") === trimmed) { + trimmed = ""; + } + + node.value = trimmed; + }); + + localStorage['X-Send-Message'] = form.message.value; + var radio = form.domain; + for(var i = 0; i < radio.length; i++) { + if(radio[i].checked) { + localStorage['X-Send-Domain'] = radio[i].value; + } + } + }) + .each(function (i, node) { + placeholder(node); + }); + + //Make sure we set up initial state + onHashChange(); + }); +});