forked from snarfed/bridgy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
wordpress_rest.py
239 lines (190 loc) · 7.99 KB
/
wordpress_rest.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""WordPress REST API (including WordPress.com) hosted blog implementation.
To use, go to your WordPress.com blog's admin console, then go to Appearance,
Widgets, add a Text widget, and put this in its text section:
<a href="https://brid.gy/webmention/wordpress" rel="webmention"></a>
(not this, it breaks :/)
<link rel="webmention" href="https://brid.gy/webmention/wordpress">
https://developer.wordpress.com/docs/api/
create returns id, can lookup by id
test command line:
curl localhost:8080/webmention/wordpress \
-d 'source=http://localhost/response.html&target=http://ryandc.wordpress.com/2013/03/24/mac-os-x/'
making an API call with an access token from the command line:
curl -H 'Authorization: Bearer [TOKEN]' URL...
"""
__author__ = ['Ryan Barrett <bridgy@ryanb.org>']
import collections
import json
import logging
import urllib
import urllib2
import urlparse
import appengine_config
from oauth_dropins import wordpress_rest as oauth_wordpress
import models
import superfeedr
import util
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template
API_CREATE_COMMENT_URL = u'https://public-api.wordpress.com/rest/v1/sites/%s/posts/%d/replies/new?pretty=true'
API_POST_SLUG_URL = u'https://public-api.wordpress.com/rest/v1/sites/%s/posts/slug:%s?pretty=true'
API_SITE_URL = u'https://public-api.wordpress.com/rest/v1/sites/%s?pretty=true'
class WordPress(models.Source):
"""A WordPress blog.
The key name is the blog hostname.
"""
GR_CLASS = collections.namedtuple('FakeGrClass', ('NAME',))(NAME='WordPress.com')
SHORT_NAME = 'wordpress'
site_info = ndb.JsonProperty(compressed=True) # from /sites/$site API call
def feed_url(self):
# http://en.support.wordpress.com/feeds/
return urlparse.urljoin(self.silo_url(), 'feed/')
def silo_url(self):
return self.domain_urls[0]
def edit_template_url(self):
return urlparse.urljoin(self.silo_url(), 'wp-admin/widgets.php')
@staticmethod
def new(handler, auth_entity=None, **kwargs):
"""Creates and returns a WordPress for the logged in user.
Args:
handler: the current RequestHandler
auth_entity: oauth_dropins.wordpress.WordPressAuth
"""
auth_domain = auth_entity.key.id()
site_info = WordPress.get_site_info(handler, auth_entity)
if site_info is None:
return
urls = util.dedupe_urls(util.trim_nulls(
[site_info.get('URL'), auth_entity.blog_url]))
domains = [util.domain_from_link(u) for u in urls]
avatar = (json.loads(auth_entity.user_json).get('avatar_URL')
if auth_entity.user_json else None)
return WordPress(id=domains[0],
auth_entity=auth_entity.key,
name=auth_entity.user_display_name(),
picture=avatar,
superfeedr_secret=util.generate_secret(),
url=urls[0],
domain_urls=urls,
domains=domains,
site_info=site_info,
**kwargs)
def _urls_and_domains(self, auth_entity):
"""Returns this blog's URL and domain.
Args:
auth_entity: oauth_dropins.wordpress_rest.WordPressAuth, unused
Returns: ([string url], [string domain])
"""
return [self.url], [self.key.id()]
def create_comment(self, post_url, author_name, author_url, content):
"""Creates a new comment in the source silo.
If the last part of the post URL is numeric, e.g. http://site/post/123999,
it's used as the post id. Otherwise, we extract the last part of
the path as the slug, e.g. http: / / site / post / the-slug,
and look up the post id via the API.
Args:
post_url: string
author_name: string
author_url: string
content: string
Returns: JSON response dict with 'id' and other fields
"""
auth_entity = self.auth_entity.get()
logging.info('Determining WordPress.com post id for %s', post_url)
# extract the post's slug and look up its post id
path = urlparse.urlparse(post_url).path
if path.endswith('/'):
path = path[:-1]
slug = path.split('/')[-1]
try:
post_id = int(slug)
except ValueError:
logging.info('Looking up post id for slug %s', slug)
url = API_POST_SLUG_URL % (auth_entity.blog_id, slug)
post_id = self.urlopen(auth_entity, url).get('ID')
if not post_id:
return self.error('Could not find post id')
logging.info('Post id is %d', post_id)
# create the comment
url = API_CREATE_COMMENT_URL % (auth_entity.blog_id, post_id)
content = u'<a href="%s">%s</a>: %s' % (author_url, author_name, content)
data = {'content': content.encode('utf-8')}
try:
resp = self.urlopen(auth_entity, url, data=urllib.urlencode(data))
except urllib2.HTTPError, e:
code, body = util.interpret_http_exception(e)
try:
parsed = json.loads(body) if body else {}
if ((code == '400' and parsed.get('error') == 'invalid_input') or
(code == '403' and parsed.get('message') == 'Comments on this post are closed')):
return parsed # known error: https://github.com/snarfed/bridgy/issues/161
except ValueError:
pass # fall through
raise e
resp['id'] = resp.pop('ID', None)
return resp
@classmethod
def get_site_info(cls, handler, auth_entity):
"""Fetches the site info from the API.
Args:
handler: the current RequestHandler
auth_entity: oauth_dropins.wordpress.WordPressAuth
Returns: site info dict, or None if API calls are disabled for this blog
"""
try:
return cls.urlopen(auth_entity, API_SITE_URL % auth_entity.blog_id)
except urllib2.HTTPError, e:
code, body = util.interpret_http_exception(e)
if (code == '403' and '"API calls to this blog have been disabled."' in body):
handler.messages.add(
'You need to <a href="http://jetpack.me/support/json-api/">enable '
'the Jetpack JSON API</a> in %s\'s WordPress admin console.' %
util.pretty_link(auth_entity.blog_url))
handler.redirect('/')
return None
raise
@staticmethod
def urlopen(auth_entity, url, **kwargs):
resp = auth_entity.urlopen(url, **kwargs).read()
logging.debug(resp)
return json.loads(resp)
class AddWordPress(oauth_wordpress.CallbackHandler, util.Handler):
def finish(self, auth_entity, state=None):
if auth_entity:
if int(auth_entity.blog_id) == 0:
self.messages.add(
'Please try again and choose a blog before clicking Authorize.')
return self.redirect_home_or_user_page(state)
# Check if this is a self-hosted WordPress blog
site_info = WordPress.get_site_info(self, auth_entity)
if site_info is None:
return
elif site_info.get('jetpack'):
logging.info('This is a self-hosted WordPress blog! %s %s',
auth_entity.key.id(), auth_entity.blog_id)
self.response.headers['Content-Type'] = 'text/html'
self.response.out.write(template.render(
'templates/confirm_self_hosted_wordpress.html',
{'auth_entity_key': auth_entity.key.urlsafe(), 'state': state}))
return
self.maybe_add_or_delete_source(WordPress, auth_entity, state)
class ConfirmSelfHosted(util.Handler):
def post(self):
self.maybe_add_or_delete_source(
WordPress,
ndb.Key(urlsafe=util.get_required_param(self, 'auth_entity_key')).get(),
util.get_required_param(self, 'state'))
class SuperfeedrNotifyHandler(superfeedr.NotifyHandler):
SOURCE_CLS = WordPress
application = webapp2.WSGIApplication([
# wordpress.com doesn't seem to use scope
# https://developer.wordpress.com/docs/oauth2/
('/wordpress/start', util.oauth_starter(oauth_wordpress.StartHandler).to(
'/wordpress/add')),
('/wordpress/confirm', ConfirmSelfHosted),
# This handles both add and delete. (WordPress.com only allows a single
# OAuth redirect URL.)
('/wordpress/add', AddWordPress),
('/wordpress/notify/(.+)', SuperfeedrNotifyHandler),
], debug=appengine_config.DEBUG)