forked from snarfed/bridgy
/
instagram.py
177 lines (144 loc) · 6.23 KB
/
instagram.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Instagram API code and datastore model classes.
Example post ID and links:
* id: 595990791004231349 or 595990791004231349_247678460 (suffix is user id)
* Permalink: http://instagram.com/p/hFYnd7Nha1/
* API URL: https://api.instagram.com/v1/media/595990791004231349
* Local handler path: /post/instagram/212038/595990791004231349
Example comment ID and links:
* id: 595996024371549506
No direct API URL or permalink, as far as I can tell. :/
* API URL for all comments on that picture:
https://api.instagram.com/v1/media/595990791004231349_247678460/comments
* Local handler path:
/comment/instagram/212038/595990791004231349_247678460/595996024371549506
"""
from __future__ import unicode_literals
import datetime
import json
import logging
import urlparse
import appengine_config
from granary import instagram as gr_instagram
from granary import microformats2
from granary import source as gr_source
from oauth_dropins import indieauth
# InstagramAuth entities are loaded here
from oauth_dropins import instagram as oauth_instagram
from oauth_dropins.webutil.handlers import TemplateHandler
import webapp2
from models import Source
import util
class Instagram(Source):
"""An Instagram account.
The key name is the username. Instagram usernames may have ASCII letters (case
insensitive), numbers, periods, and underscores:
https://stackoverflow.com/questions/15470180
"""
GR_CLASS = gr_instagram.Instagram
SHORT_NAME = 'instagram'
FAST_POLL = datetime.timedelta(minutes=120)
RATE_LIMITED_POLL = Source.SLOW_POLL
RATE_HTTP_LIMIT_CODES = Source.RATE_LIMIT_HTTP_CODES + ('503',)
URL_CANONICALIZER = util.UrlCanonicalizer(
domain=GR_CLASS.DOMAIN,
subdomain='www',
approve=r'https://www.instagram.com/p/[^/?]+/$',
trailing_slash=True,
headers=util.REQUEST_HEADERS)
# no reject regexp; non-private Instagram post URLs just 404
@staticmethod
def new(handler, auth_entity=None, actor=None, **kwargs):
"""Creates and returns an :class:`Instagram` for the logged in user.
Args:
handler: the current :class:`webapp2.RequestHandler`
auth_entity: :class:`oauth_dropins.instagram.InstagramAuth`
"""
user = json.loads(auth_entity.user_json)
user['actor'] = actor
auth_entity.user_json = json.dumps(user)
auth_entity.put()
username = actor['username']
if not kwargs.get('features'):
kwargs['features'] = ['listen']
urls = microformats2.object_urls(actor)
return Instagram(id=username,
auth_entity=auth_entity.key,
name=actor.get('displayName'),
picture=actor.get('image', {}).get('url'),
url=gr_instagram.Instagram.user_url(username),
domain_urls=urls,
domains=[util.domain_from_link(url) for url in urls],
**kwargs)
def silo_url(self):
"""Returns the Instagram account URL, e.g. https://instagram.com/foo."""
return self.url
def user_tag_id(self):
"""Returns the tag URI for this source, e.g. 'tag:instagram.com:123456'."""
user = json.loads(self.auth_entity.get().user_json)
return (user.get('actor', {}).get('id') or
self.gr_source.tag_uri(user.get('id') or self.key.id()))
def label_name(self):
"""Returns the username."""
return self.key.id()
def get_activities_response(self, *args, **kwargs):
"""Set user_id because scraping requires it."""
kwargs.setdefault('group_id', gr_source.SELF)
kwargs.setdefault('user_id', self.key.id())
return self.gr_source.get_activities_response(*args, **kwargs)
class StartHandler(TemplateHandler, util.Handler):
"""Serves the "Enter your username" form page."""
def template_file(self):
return 'indieauth.html'
def post(self):
ia_start = util.oauth_starter(indieauth.StartHandler).to('/instagram/callback')(
self.request, self.response)
try:
self.redirect(ia_start.redirect_url(me=util.get_required_param(self, 'user_url')))
except Exception as e:
if util.is_connection_failure(e) or util.interpret_http_exception(e)[0]:
self.messages.add("Couldn't fetch your web site: %s" % e)
return self.redirect('/')
raise
class CallbackHandler(indieauth.CallbackHandler, util.Handler):
def finish(self, auth_entity, state=None):
if auth_entity:
user_json = json.loads(auth_entity.user_json)
# find instagram profile URL
urls = user_json.get('rel-me', [])
logging.info('rel-mes: %s', urls)
for url in util.trim_nulls(urls):
if util.domain_from_link(url) == gr_instagram.Instagram.DOMAIN:
username = urlparse.urlparse(url).path.strip('/')
break
else:
self.messages.add(
'No Instagram profile found. Please <a href="https://indieauth.com/setup">'
'add an Instagram rel-me link</a>, then try again.')
return self.redirect('/')
# check that instagram profile links to web site
actor = gr_instagram.Instagram(scrape=True).get_actor(
username, ignore_rate_limit=True)
if not actor:
self.messages.add(
"Couldn't find Instagram user '%s'. Please check your site's rel-me "
"link and your Instagram account." % username)
return self.redirect('/')
canonicalize = util.UrlCanonicalizer(redirects=False)
website = canonicalize(auth_entity.key.id())
urls = [canonicalize(u) for u in microformats2.object_urls(actor)]
logging.info('Looking for %s in %s', website, urls)
if website not in urls:
self.messages.add("Please add %s to your Instagram profile's website or "
'bio field and try again.' % website)
return self.redirect('/')
# check that the instagram account is public
if not gr_source.Source.is_public(actor):
self.messages.add('Your Instagram account is private. '
'Bridgy only supports public accounts.')
return self.redirect('/')
self.maybe_add_or_delete_source(Instagram, auth_entity, state, actor=actor)
application = webapp2.WSGIApplication([
('/instagram/start', StartHandler),
('/instagram/indieauth', indieauth.StartHandler.to('/instagram/callback')),
('/instagram/callback', CallbackHandler),
], debug=appengine_config.DEBUG)