-
Notifications
You must be signed in to change notification settings - Fork 57
/
Copy pathblogger.py
229 lines (183 loc) · 7.69 KB
/
blogger.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
"""Blogger API 2.0 hosted blog implementation.
Blogger API docs:
https://developers.google.com/blogger/docs/2.0/developers_guide_protocol
Python GData API docs:
http://gdata-python-client.googlecode.com/hg/pydocs/gdata.blogger.data.html
To use, go to your Blogger blog's dashboard, click Template, Edit HTML, then
put this in the head section::
<link rel="webmention" href="https://brid.gy/webmention/blogger"></link>
* https://developers.google.com/blogger/docs/2.0/developers_guide_protocol
* https://support.google.com/blogger/answer/42064?hl=en
* https://developers.google.com/blogger/docs/2.0/developers_guide_protocol#CreatingComments
Test command line::
curl localhost:8080/webmention/blogger -d 'source=http://localhost/response.html&target=http://freedom-io-2.blogspot.com/2014/04/blog-post.html'
"""
import collections
import logging
import re
import urllib.parse
from flask import render_template, request
from gdata.blogger.client import Query
from gdata.client import Error
from google.cloud import ndb
from oauth_dropins import blogger as oauth_blogger
from oauth_dropins.webutil.flask_util import flash
from flask_app import app
import models
import superfeedr
import util
logger = logging.getLogger(__name__)
# Blogger says it's 4096 in an error message. (Couldn't find it in their docs.)
# We include some padding.
# Background: https://github.com/snarfed/bridgy/issues/242
MAX_COMMENT_LENGTH = 4000
class Blogger(models.Source):
"""A Blogger blog.
The key name is the blog id.
"""
GR_CLASS = collections.namedtuple('FakeGrClass', ('NAME',))(NAME='Blogger')
OAUTH_START = oauth_blogger.Start
SHORT_NAME = 'blogger'
PATH_BLOCKLIST = (re.compile('^/search/.*'),)
def feed_url(self):
# https://support.google.com/blogger/answer/97933?hl=en
return urllib.parse.urljoin(self.url, '/feeds/posts/default') # Atom
def silo_url(self):
return self.url
def edit_template_url(self):
return f'https://www.blogger.com/blogger.g?blogID={self.key_id()}#template'
@staticmethod
def new(auth_entity=None, blog_id=None, **kwargs):
"""Creates and returns a Blogger for the logged in user.
Args:
auth_entity (oauth_dropins.blogger.BloggerV2Auth):
blog_id (str): which blog, optional; if not provided, uses the first
available
"""
urls, domains = Blogger.urls_and_domains(auth_entity, blog_id=blog_id)
if not urls or not domains:
flash('Blogger blog not found. Please create one first!')
return None
if blog_id is None:
for blog_id, hostname in zip(auth_entity.blog_ids, auth_entity.blog_hostnames):
if domains[0] == hostname:
break
else:
assert False, "Internal error, shouldn't happen"
return Blogger(id=blog_id,
auth_entity=auth_entity.key,
url=urls[0],
name=auth_entity.user_display_name(),
domains=domains,
domain_urls=urls,
picture=auth_entity.picture_url,
superfeedr_secret=util.generate_secret(),
**kwargs)
@staticmethod
def urls_and_domains(auth_entity, blog_id=None):
"""Returns an auth entity's URL and domain.
Args:
auth_entity (oauth_dropins.blogger.BloggerV2Auth):
blog_id: which blog. optional. if not provided, uses the first available.
Returns:
([str url], [str domain])
"""
for id, host in zip(auth_entity.blog_ids, auth_entity.blog_hostnames):
if blog_id == id or (not blog_id and host):
return [f'http://{host}/'], [host]
return [], []
def create_comment(self, post_url, author_name, author_url, content, client=None):
"""Creates a new comment in the source silo.
Must be implemented by subclasses.
Args:
post_url (str)
author_name (str)
author_url (str)
content (str)
client (gdata.blogger.client.BloggerClient): If None, one will be
created from auth_entity. Used for dependency injection in the unit
test.
Returns:
dict: JSON response with ``id`` and other fields
"""
if client is None:
client = self.auth_entity.get().api()
# extract the post's path and look up its post id
path = urllib.parse.urlparse(post_url).path
logger.info(f'Looking up post id for {path}')
feed = client.get_posts(self.key_id(), query=Query(path=path))
if not feed.entry:
return self.error(f'Could not find Blogger post {post_url}')
elif len(feed.entry) > 1:
logger.warning(f'Found {len(feed.entry)} Blogger posts for path {path} , expected 1')
post_id = feed.entry[0].get_post_id()
# create the comment
content = f'<a href="{author_url}">{author_name}</a>: {content}'
if len(content) > MAX_COMMENT_LENGTH:
content = content[:MAX_COMMENT_LENGTH - 3] + '...'
logger.info(f"Creating comment on blog {self.key.id()}, post {post_id}: {content.encode('utf-8')}")
try:
comment = client.add_comment(self.key.id(), post_id, content)
except Error as e:
msg = str(e)
if ('Internal error:' in msg):
# known errors. e.g. https://github.com/snarfed/bridgy/issues/175
# https://groups.google.com/d/topic/bloggerdev/szGkT5xA9CE/discussion
return {'error': msg}
else:
raise
resp = {'id': comment.get_comment_id(), 'response': comment.to_string()}
logger.info(f'Response: {resp}')
return resp
@app.route('/blogger/oauth_handler')
def oauth_callback():
"""OAuth callback handler.
Both the add and delete flows have to share this because Blogger's
oauth-dropin doesn't yet allow multiple callback handlers. :/
"""
auth_entity = None
auth_entity_str_key = request.values.get('auth_entity')
if auth_entity_str_key:
auth_entity = ndb.Key(urlsafe=auth_entity_str_key).get()
if not auth_entity.blog_ids or not auth_entity.blog_hostnames:
auth_entity = None
if not auth_entity:
flash("Couldn't fetch your blogs. Maybe you're not a Blogger user?")
state = request.values.get('state')
if not state:
state = util.construct_state_param_for_add(feature='webmention')
if not auth_entity:
util.maybe_add_or_delete_source(Blogger, auth_entity, state)
return
vars = {
'action': '/blogger/add',
'state': state,
'operation': util.decode_oauth_state(state).get('operation'),
'auth_entity_key': auth_entity.key.urlsafe().decode(),
'blogs': [{'id': id, 'title': title, 'domain': host}
for id, title, host in zip(auth_entity.blog_ids,
auth_entity.blog_titles,
auth_entity.blog_hostnames)],
}
logger.info(f'Rendering choose_blog.html with {vars}')
return render_template('choose_blog.html', **vars)
@app.route('/blogger/add', methods=['POST'])
def blogger_add():
util.maybe_add_or_delete_source(
Blogger,
ndb.Key(urlsafe=request.form['auth_entity_key']).get(),
request.form['state'],
blog_id=request.form['blog'],
)
class SuperfeedrNotify(superfeedr.Notify):
SOURCE_CLS = Blogger
# Blogger only has one OAuth scope. oauth-dropins fills it in.
# https://developers.google.com/blogger/docs/2.0/developers_guide_protocol#OAuth2Authorizing
start = util.oauth_starter(oauth_blogger.Start).as_view(
'blogger_start', '/blogger/oauth2callback')
app.add_url_rule('/blogger/start', view_func=start, methods=['POST'])
app.add_url_rule('/blogger/oauth2callback', view_func=oauth_blogger.Callback.as_view(
'blogger_oauth2callback', '/blogger/oauth_handler'))
app.add_url_rule('/blogger/delete/start', view_func=oauth_blogger.Start.as_view(
'blogger_delete_start', '/blogger/oauth2callback'))
app.add_url_rule('/blogger/notify/<id>', view_func=SuperfeedrNotify.as_view('blogger_notify'), methods=['POST'])