Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial LinkedIn OAuth support. #10

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3ce1d57
Initial commit of linkedin support.
dpnova Aug 20, 2011
7c351bb
Remove some noisy debug and fix broken import.
dpnova Aug 20, 2011
9af3063
Small fixes and removal of siangls from twitter app.
dpnova Aug 21, 2011
a5e9987
Get the twitter avatar for the current user.
dpnova Sep 9, 2011
8a898b0
Merge branch 'master' of https://github.com/dgouldin/django-socialite
dpnova Sep 9, 2011
cc5197a
Add avatar template tag for linkedin.
dpnova Sep 9, 2011
1acb142
Fixes to linked in helpers and template tags.
dpnova Sep 10, 2011
64f2cd9
Facebook get_avatar helper.
dpnova Sep 11, 2011
9d027b9
Add template tags for getting avatar for facebook.
dpnova Sep 11, 2011
c7af71c
Fix small bug in the facebook avatar function.
dpnova Sep 18, 2011
9b73f34
get_friend_info working properly and removed some debug.
dpnova Sep 18, 2011
3415663
Post a link to someone's wall.
dpnova Sep 18, 2011
22d8357
Add picture to post to facebook
dpnova Oct 1, 2011
84189b1
* Let us pass headers through in oauth requests.
dpnova Oct 5, 2011
d005ac5
Small fixes, noteworthy are bad usage of memcached.
dpnova Oct 11, 2011
9a26dff
merge
dpnova Oct 11, 2011
0cb5fac
Fix redirect bug in authorize.
dpnova Oct 12, 2011
ce67827
Remove noisy debug.
dpnova Oct 12, 2011
a380f08
Fix redirect bug in authorize in oauth2.
dpnova Oct 12, 2011
683df94
Sometimes we get long names from linkedin.
dpnova Oct 27, 2011
c220e9b
Merge branch 'master' of https://github.com/dpnova/django-socialite
dpnova Oct 27, 2011
425e96c
Better errors and get connections for linkedin.
dpnova Feb 22, 2012
ee388d4
Better errors and get connections for linkedin. merge
dpnova Feb 23, 2012
4867396
Let announce work for pages and user ids.
dpnova Mar 18, 2012
8dd2c36
Let us request extra data from linkedin.
dpnova Apr 5, 2012
0181902
Dont redirect here.
dpnova Sep 9, 2012
3d1ba7d
Make messages usage 1.4 friendly
dpnova Sep 9, 2012
4ec4efd
More helpful errors. Only margionally though.
dpnova Sep 9, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion socialite/apps/base/oauth/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def register_user(self, access_token):
def authenticate(self, client=None, access_token=None, impersonate=None):
if client is None or access_token is None or not self.validate_service_type(getattr(client, 'base_url', None)):
return None
user = self.get_existing_user(access_token, impersonate=impersonate)
user = self.get_existing_user(access_token)
if user:
return user
import logging
Expand Down
8 changes: 7 additions & 1 deletion socialite/apps/base/oauth/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ def authorize(self, view_function):
self.view_functions[helper.AUTHORIZE] = view_function
@login_required
def _authorize(request):
redirect_to = request.REQUEST.get(self.redirect_field_name, '')
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL
request.session['redirect_to'] = redirect_to
return self._redirect(request, helper.AUTHORIZE)
return _authorize

Expand All @@ -62,5 +67,6 @@ def _authenticate(request):
if request.user.is_authenticated():
return HttpResponseRedirect(redirect_to)
request.session['redirect_to'] = redirect_to
return self._redirect(request, helper.AUTHENTICATE)
redirect_url = self._redirect(request, helper.AUTHENTICATE)
return redirect_url
return _authenticate
6 changes: 3 additions & 3 deletions socialite/apps/base/oauth/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get_url(self, action, token=None, callback_url=None):
url = urlparse.urljoin(self.base_url, self.actions[action])
except KeyError:
raise Exception("Invalid OAuth action.")

if token:
if self.signature_method:
consumer = oauth.Consumer(self.key, self.secret)
Expand Down Expand Up @@ -73,11 +73,11 @@ def access_token(self, request_token, verifier=None):
raise Exception("Invalid response %s." % resp['status'])
return dict(urlparse.parse_qsl(content))

def request(self, url, access_token, method="GET", body=''):
def request(self, url, access_token, method="GET", body='', headers=None):
consumer = oauth.Consumer(self.key, self.secret)
token = oauth.Token(key=access_token['oauth_token'], secret=access_token['oauth_token_secret'])
client = oauth.Client(consumer, token=token)
resp, content = client.request(url, method=method, body=body)
resp, content = client.request(url, method=method, body=body, headers=headers)
if resp['status'] != '200':
raise Exception("Invalid response %s.\r\n%s" % (resp['status'], content))
return content
11 changes: 9 additions & 2 deletions socialite/apps/base/oauth20/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ def login(self, request, access_token):
if user:
django_login(request, user)
return user

def callback(self, request):
action = request.session.get(ACTION_FORMAT % self.client.oauth_base_url)
view_function = self.view_functions.get(action)
if not view_function:
raise Exception("OAuth callback was not able to retrieve the needed information from the session.")

access_token = request.GET.get('access_token')
code = request.GET.get('code')
if code and access_token is None:
Expand All @@ -49,6 +49,13 @@ def authorize(self, view_function):
self.view_functions[helper.AUTHORIZE] = view_function
@login_required
def _authorize(request):
redirect_to = request.REQUEST.get(self.redirect_field_name, '')
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL
# if request.user.is_authenticated():
# return HttpResponseRedirect(redirect_to)
request.session['redirect_to'] = redirect_to
request.session[ACTION_FORMAT % self.client.oauth_base_url] = helper.AUTHORIZE
redirect_uri = request.build_absolute_uri(reverse(self.callback))
return HttpResponseRedirect(self.client.authorization_url(params=self.params, redirect_uri=redirect_uri))
Expand Down
File renamed without changes.
59 changes: 56 additions & 3 deletions socialite/apps/facebook/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ def add_padding(s):
return view(request, data, *args, **kwargs)
return _view


import hashlib
def users_info(access_token, ids):
CACHE_KEY = 'facebook:users_info:%s:%s' % (access_token, ','.join([str(i) for i in ids]))
CACHE_KEY = 'facebook:users_info:%s:%s' % (access_token, hashlib.md5(str(','.join([str(i) for i in ids]))).hexdigest())
info = cache.get(CACHE_KEY)
if info is None:
base_uri = api_url
Expand Down Expand Up @@ -98,19 +100,70 @@ def get_friend_ids(access_token):
cache.set(CACHE_KEY, info, 60 * 5) # 5 minutes
return info

def get_friend_info(access_token):
CACHE_KEY = 'facebook:get_friend_info:%s' % access_token
info = cache.get(CACHE_KEY)
if info is None:
base_uri = urlparse.urljoin(api_url, 'me/friends')
fields = [
'id',
'first_name',
'last_name',
'name',
'picture',
]
params = {
'fields': ','.join(fields),
}
r,c = oauth_client.request(base_uri, access_token=access_token, params=params)
# TODO: handle response != 200
friends = simplejson.loads(c)
info = friends['data']
cache.set(CACHE_KEY, info, 60 * 5) # 5 minutes
return info

def find_friends(access_token):
facebook_ids = get_friend_ids(access_token)
friends = []
if facebook_ids:
friends = models.FacebookService.objects.filter(unique_id__in=facebook_ids)
return friends

def announce(access_token, message):
base_uri = urlparse.urljoin(api_url, 'me/feed')
def announce(access_token, message, user_id="me"):
base_uri = urlparse.urljoin(api_url, '%s/feed' % user_id)
q = get_mutable_query_dict({
'access_token': access_token,
'message': message,
})
r,c = oauth_client.request(base_uri, access_token=access_token, method="POST", body=q.urlencode())
# TODO: handle response != 200
return simplejson.loads(c)


def link(access_token, link, message=None,picture=None, user_id="me"):
base_uri = urlparse.urljoin(api_url, '%s/feed' % user_id)
q = {
'access_token': access_token,
'message': message,
'link':link
}
if picture:
q['picture'] = picture
q = get_mutable_query_dict(q)
r,c = oauth_client.request(base_uri, access_token=access_token, method="POST", body=q.urlencode())
# TODO: handle response != 200
return simplejson.loads(c)

FACEBOOK_IMAGE_SIZES = set(['square','small','normal','large'])
def get_avatar(access_token, user_id="me", size="large"):
if size not in FACEBOOK_IMAGE_SIZES:
raise ValueError("size must be one of %s" % FACEBOOK_IMAGE_SIZES)
base_uri = urlparse.urljoin(api_url, '%s/picture' % (user_id,))
"""
The following is here because oauth.client.request tries to fetch the image
which seems to block. Instead we just return the url.
"""
params = {'type':size}
params['oauth_token'] = access_token
uri = '%s?%s' % (base_uri, urllib.urlencode(params))
return uri
Empty file.
11 changes: 11 additions & 0 deletions socialite/apps/facebook/templatetags/facebook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django import template
from socialite.apps.facebook.helper import get_avatar

register = template.Library()


@register.simple_tag()
def facebook_avatar(size, user, user_id="me"):
return get_avatar(access_token=user.facebookservices.all()[0].access_token,
user_id=user_id,
size=size)
6 changes: 4 additions & 2 deletions socialite/apps/facebook/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib import messages

from socialite.apps.base.oauth20 import decorators as oauth_decorators

Expand All @@ -25,7 +26,8 @@ def authorize(request, access_token, redirect_to=settings.LOGIN_REDIRECT_URL):
message = "Facebook account added."
else:
message = "This Facebook account has already been adeed."
request.user.message_set.create(message=message)
messages.add_message(request, messages.INFO, message)
# request.user.message_set.create(message=message)
return HttpResponseRedirect(redirect_to)

@mediator.authenticate
Expand All @@ -36,7 +38,7 @@ def authenticate(request, access_token, redirect_to=settings.LOGIN_REDIRECT_URL)
'unique_id': helper.get_unique_id(access_token),
})
return HttpResponseRedirect(redirect_to)
return HttpResponse('fail!') # TODO: real response
return HttpResponse('fail! This often happens because you dont have the auth backend installed.') # TODO: real response

@helper.signed
def canvas(request, data, template_name='facebook/canvas.html', extra_context=None):
Expand Down
Empty file.
32 changes: 32 additions & 0 deletions socialite/apps/linkedin/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import random
import hashlib

from django.contrib.auth.models import User

from socialite.apps.base.oauth.backends import BaseOauthBackend
from socialite.apps.base.oauth20.utils import get_unique_username
from socialite.apps.linkedin import helper, models
from socialite.apps.linkedin.registration import register_service

class LinkedInBackend(BaseOauthBackend):
def validate_service_type(self, base_url):
return base_url == helper.oauth_client.base_url

def get_existing_user(self, access_token):
unique_id = helper.get_unique_id(access_token)
try:
service = models.LinkedInService.objects.get(unique_id=unique_id)
except models.LinkedInService.DoesNotExist:
return None
return service.user

def register_user(self, access_token, impersonate=None):
try:
user_info = helper.user_info(access_token)
except: # TODO: bare except, bad!
import traceback;traceback.print_exc()
return None


return register_service(user_info, unique_id=user_info['id'],
access_token=access_token, impersonate=impersonate).user
75 changes: 75 additions & 0 deletions socialite/apps/linkedin/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from django.conf import settings
from socialite.apps.base.oauth import helper as oauth_helper
import urlparse
import urllib
from django.core.urlresolvers import reverse
from oauth2 import SignatureMethod_HMAC_SHA1
try:
import json
except ImportError:
import simplejson as json

api_url = "https://api.linkedin.com/v1/"
oauth_url = "https://www.linkedin.com/uas/oauth/"

oauth_actions = {
oauth_helper.REQUEST_TOKEN: 'requestToken',
oauth_helper.AUTHORIZE: 'authorize',
oauth_helper.AUTHENTICATE: 'authenticate',
oauth_helper.ACCESS_TOKEN: 'accessToken',
}
oauth_client = oauth_helper.Client(settings.LINKEDIN_KEY, settings.LINKEDIN_SECRET, oauth_url, oauth_actions)
# this should work being passed to the above, but linked in docs conflict with themselves
#, signature_method=SignatureMethod_HMAC_SHA1())


def get_unique_id(access_token, user_id=None):
try:
return access_token['user_id']
except KeyError:
pass
return user_info(access_token, user_id=user_id)['id']

def user_info(access_token, user_id=None,extra_fields=""):
if user_id is None:
url = '%s?%s' % (urlparse.urljoin(api_url, 'people/~:(id,first-name,last-name,picture-url%s)'%extra_fields),"format=json")
else:
url = '%s?%s' % (urlparse.urljoin(api_url, 'people/id=%s:(id,first-name,last-name,picture-url)'%user_id),"format=json")
info = json.loads(oauth_client.request(url, access_token))
return info


def get_avatar(access_token=None,user_id=None):
info = user_info(access_token,user_id)
return info['pictureUrl']


def get_connections(access_token):
url = "http://api.linkedin.com/v1/people/~/connections?format=json"
connection_data = oauth_client.request(url,access_token)
print connection_data
connections = json.loads(connection_data)
return connections

def announce(access_token, link, message, picture, title):
url = "http://api.linkedin.com/v1/people/~/shares"
xml = """<?xml version="1.0" encoding="UTF-8"?>
<share>
<comment>%s</comment>
<content>
<title>%s</title>
<submitted-url>%s</submitted-url>
<submitted-image-url>%s</submitted-image-url>
</content>
<visibility>
<code>anyone</code>
</visibility>
</share>
""" % (message, title, link, picture)
try:
r,c = oauth_client.request(url, access_token=access_token, method="POST", body=xml, headers={'Content-Type':'text/xml'})
except Exception,e:
if "201" not in repr(e):
raise
return True

20 changes: 20 additions & 0 deletions socialite/apps/linkedin/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.db import models
from socialite.apps.base.oauth.models import OauthService


class LinkedInServiceManager(models.Manager):
def update_or_create(self, **kwargs):
obj, created = self.get_or_create(**kwargs)
if not created and 'defaults' in kwargs:
for k,v in kwargs['defaults'].iteritems():
setattr(obj, k, v)
obj.save()
return obj, created


class LinkedInService(OauthService):
screen_name = models.CharField(max_length=20)
display_name = models.CharField(max_length=255)
updated = models.DateTimeField(auto_now=True)

objects = LinkedInServiceManager()
39 changes: 39 additions & 0 deletions socialite/apps/linkedin/registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import random
import hashlib

from django.contrib.auth.models import User

from socialite.apps.base.oauth.utils import get_unique_username
from socialite.apps.linkedin import models
from socialite.apps.base.signals import post_register_service

def register_service(user_info, unique_id=None, access_token=None, impersonate=None):
# FIXME: massive race condition potential here
unique_id = unique_id or user_info['id']

try:
service = models.LinkedInService.objects.get(unique_id=unique_id)
except models.LinkedInService.DoesNotExist:
user = User(password=hashlib.md5(str(random.random())).hexdigest())
service = models.LinkedInService()
else:
user = service.user

screen_name = ''.join((user_info['firstName'], user_info['lastName']))
if not user.username:
user.username = get_unique_username(screen_name)
if not user.first_name:
user.first_name = user_info['firstName']
if not user.last_name:
user.last_name = user_info['lastName']
user.save()

# update service
service.user = user
service.unique_id = unique_id
service.screen_name = screen_name[:20]
service.impersonated_unique_id = impersonate or ''
service.access_token = access_token
service.save()
post_register_service.send(sender=models.LinkedInService, instance=service, user_info=user_info)
return service
Empty file.
9 changes: 9 additions & 0 deletions socialite/apps/linkedin/templatetags/linkedin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django import template
from socialite.apps.linkedin.helper import get_avatar

register = template.Library()


@register.simple_tag()
def linkedin_avatar(user, user_id=None):
return get_avatar(access_token=user.linkedinservices.all()[0].access_token, user_id=user_id)
8 changes: 8 additions & 0 deletions socialite/apps/linkedin/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.test.testcases import TestCase



class TestLinkedInOAuth(TestCase):
def test_authenticate(self):
resp = self.client.get('/auth/linkedin/authenticate/')
print resp
Loading