def handle_new_or_edit(post, message, link, name, picture, is_photo, album_id): current_app.logger.debug("publishing to facebook") # TODO I cannot figure out how to tag people via the FB API post_args = { "access_token": get_settings().facebook_access_token, "message": message.strip(), "privacy": json.dumps({"value": "EVERYONE"}), #'privacy': json.dumps({'value': 'SELF'}), } if is_photo and picture: post_args["url"] = picture current_app.logger.debug("Sending photo %s to album %s", post_args, album_id) response = requests.post( "https://graph.facebook.com/v2.0/{}/photos".format(album_id if album_id else "me"), data=post_args ) else: post_args.update(util.trim_nulls({"link": link, "name": name, "picture": picture})) current_app.logger.debug("Sending post %s", post_args) response = requests.post("https://graph.facebook.com/v2.0/me/feed", data=post_args) response.raise_for_status() current_app.logger.debug("Got response from facebook %s", response) if "json" in response.headers["content-type"]: result = response.json() current_app.logger.debug("published to facebook. response {}".format(result)) if result: if is_photo: facebook_photo_id = result["id"] facebook_post_id = result["post_id"] # actually the album split = facebook_post_id.split("_", 1) if split and len(split) == 2: user_id, post_id = split fb_url = "https://facebook.com/{}/posts/{}".format(user_id, facebook_photo_id) post.add_syndication_url(fb_url) return fb_url else: facebook_post_id = result["id"] split = facebook_post_id.split("_", 1) if split and len(split) == 2: user_id, post_id = split fb_url = "https://facebook.com/{}/posts/{}".format(user_id, post_id) post.add_syndication_url(fb_url) return fb_url
def handle_new_or_edit(post, message, link, name, picture, is_photo, album_id): current_app.logger.debug('publishing to facebook') # TODO I cannot figure out how to tag people via the FB API post_args = { 'access_token': get_settings().facebook_access_token, 'message': message.strip(), 'privacy': json.dumps({'value': 'EVERYONE'}), #'privacy': json.dumps({'value': 'SELF'}), } if is_photo and picture: post_args['url'] = picture current_app.logger.debug( 'Sending photo %s to album %s', post_args, album_id) response = requests.post( 'https://graph.facebook.com/v2.0/{}/photos'.format( album_id if album_id else 'me'), data=post_args) else: post_args.update(util.trim_nulls({ 'link': link, 'name': name, 'picture': picture, })) current_app.logger.debug('Sending post %s', post_args) response = requests.post('https://graph.facebook.com/v2.0/me/feed', data=post_args) response.raise_for_status() current_app.logger.debug("Got response from facebook %s", response) if 'json' in response.headers['content-type']: result = response.json() current_app.logger.debug( 'published to facebook. response {}'.format(result)) if result: if is_photo: facebook_photo_id = result['id'] facebook_post_id = result['post_id'] # actually the album split = facebook_post_id.split('_', 1) if split and len(split) == 2: user_id, post_id = split fb_url = 'https://facebook.com/{}/posts/{}'.format( user_id, facebook_photo_id) post.add_syndication_url(fb_url) return fb_url else: facebook_post_id = result['id'] split = facebook_post_id.split('_', 1) if split and len(split) == 2: user_id, post_id = split fb_url = 'https://facebook.com/{}/posts/{}'.format( user_id, post_id) post.add_syndication_url(fb_url) return fb_url
def do_syndicate(post_id, target_id, app_config): with async_app_context(app_config): post = Post.query.get(post_id) target = PosseTarget.query.get(target_id) current_app.logger.debug( 'posseing %s to target %s', post.path, target.uid) data = {'access_token': target.access_token} files = None if post.repost_of: data['repost-of'] = post.repost_of[0] if post.like_of: data['like-of'] = post.like_of[0] if post.in_reply_to: data['in-reply-to'] = post.in_reply_to[0] if post.post_type == 'review': item = post.item or {} data['item[name]'] = data['item'] = item.get('name') data['item[author]'] = item.get('author') data['rating'] = post.rating data['description'] = data['description[markdown]'] = data['description[value]'] = post.content data['description[html]'] = post.content_html else: data['name'] = post.title data['content'] = data['content[markdown]'] = data['content[value]'] = post.content data['content[html]'] = post.content_html data['url'] = (post.shortlink if target.style == 'microblog' else post.permalink) if post.post_type == 'photo' and post.attachments: if len(post.attachments) == 1: a = post.attachments[0] files = {'photo': (a.filename, open(a.disk_path, 'rb'), a.mimetype)} else: files = [('photo[]', (a.filename, open(a.disk_path, 'rb'), a.mimetype)) for a in post.attachments] data['location'] = post.get_location_as_geo_uri() data['place-name'] = post.venue and post.venue.name categories = [tag.name for tag in post.tags] for person in post.people: categories.append(person.url) if person.social: categories += person.social data['category[]'] = categories resp = requests.post(target.micropub_endpoint, data=util.trim_nulls(data), files=files) resp.raise_for_status() current_app.logger.debug( 'received response from posse endpoint: code=%d, headers=%s, body=%s', resp.status_code, resp.headers, resp.text) post.add_syndication_url(resp.headers['Location']) db.session.commit()
def micropub_endpoint(): current_app.logger.info( "received micropub request %s, args=%s, form=%s, headers=%s", request, request.args, request.form, request.headers) bearer_prefix = 'Bearer ' header_token = request.headers.get('authorization') if header_token and header_token.startswith(bearer_prefix): token = header_token[len(bearer_prefix):] else: token = request.form.get('access_token') if not token: current_app.logger.warn('hit micropub endpoint with no access token') abort(401) try: decoded = util.jwt_decode(token) except jwt.DecodeError as e: current_app.logger.warn('could not decode access token: %s', e) abort(401) me = decoded.get('me') client_id = decoded.get('client_id') cred = Credential.query.filter_by(type='indieauth', value=me).first() user = cred and cred.user if not user or not user.is_authenticated(): current_app.logger.warn( 'received valid access token for invalid user: %s', me) abort(401) if request.method == 'GET': current_app.logger.debug('micropub GET request %s -> %s', request, request.args) accept_header = request.headers.get('accept', '') q = request.args.get('q') if q == 'syndicate-to': current_app.logger.debug('returning syndication targets') if 'application/json' in accept_header: return jsonify({ 'syndicate-to': util.trim_nulls([{ 'uid': t.uid, 'name': t.name, 'user': { 'name': t.user_name, 'url': t.user_url, 'photo': t.user_photo, }, 'service': { 'name': t.service_name, 'url': t.service_url, 'photo': t.service_photo, }, } for t in user.posse_targets]) }) else: response = make_response(urllib.parse.urlencode([ ('syndicate-to[]', t.uid) for t in user.posse_targets])) response.headers['Content-Type'] = 'application/x-www-form-urlencoded' return response elif q in ('actions', 'json_actions'): current_app.logger.debug('returning action handlers') reply_url = url_for('admin.new_post', type='reply', _external=True) repost_url = url_for('admin.new_post', type='share', _external=True) like_url = url_for('admin.new_post', type='like', _external=True) payload = { 'reply': reply_url + '?url={url}', 'repost': repost_url + '?url={url}', 'favorite': like_url + '?url={url}', 'like': like_url + '?url={url}', } if q == 'json_actions' or 'application/json' in accept_header: return jsonify(payload) else: response = make_response(urllib.parse.urlencode(payload)) response.headers['Content-Type'] = 'application/x-www-form-urlencoded' return response else: abort(404) h = request.form.get('h') in_reply_to = request.form.get('in-reply-to') like_of = request.form.get('like-of') photo_url = request.form.get('photo') photo_file = request.files.get('photo') bookmark = request.form.get('bookmark') or request.form.get('bookmark-of') repost_of = request.form.get('repost-of') post_type = ('event' if h == 'event' else 'article' if 'name' in request.form else 'photo' if photo_file or photo_url else 'reply' if in_reply_to else 'like' if like_of else 'bookmark' if bookmark else 'share' if repost_of else 'note') latitude = None longitude = None location_name = None venue_id = None loc_str = request.form.get('location') geo_prefix = 'geo:' if loc_str: if loc_str.startswith(geo_prefix): loc_str = loc_str[len(geo_prefix):] loc_params = loc_str.split(';') if loc_params: latitude, longitude = loc_params[0].split(',', 1) location_name = request.form.get('place_name') else: venue_prefix = urllib.parse.urljoin(get_settings().site_url, 'venues/') if loc_str.startswith(venue_prefix): slug = loc_str[len(venue_prefix):] venue = Venue.query.filter_by(slug=slug).first() if venue: venue_id = venue.id # url of the venue, e.g. https://kylewm.com/venues/cafe-trieste-berkeley-california venue = request.form.get('venue') syndicate_to = request.form.getlist('syndicate-to[]') syndication = request.form.getlist('syndication[]') if syndication: syndication = '\n'.join(syndication) else: syndication = request.form.get('syndication') # TODO check client_id if syndication: current_app.logger.debug( 'checking for existing post with syndication %s', syndication) existing = Post.query.filter( Post.syndication.like(db.literal('%"' + syndication + '"%')), ~Post.deleted ).first() if existing: current_app.logger.debug( 'found post for %s: %s', syndication, existing) return redirect(existing.permalink) else: current_app.logger.debug( 'no post found with syndication %s', syndication) # translate from micropub's verbage.TODO unify translated = util.trim_nulls({ 'post_type': post_type, 'published': request.form.get('published'), 'start': request.form.get('start'), 'end': request.form.get('end'), 'title': request.form.get('name'), 'content': request.form.get('content[html]') or request.form.get('content'), 'venue': venue_id, 'latitude': latitude, 'longitude': longitude, 'location_name': location_name, 'syndication': syndication, 'in_reply_to': in_reply_to, 'like_of': like_of, 'repost_of': repost_of, 'bookmark_of': bookmark, 'photo': photo_file or photo_url, 'syndicate-to': syndicate_to, 'hidden': 'true' if like_of or bookmark else 'false', }) with current_app.test_request_context( base_url=get_settings().site_url, path='/save_new', method='POST', data=translated ): current_app.logger.debug('received fake request %s: %s', request, request.args) login_user(user) current_app.logger.debug('successfully authenticated as user %s => %s', me, user) from . import admin resp = admin.save_new() return make_response('Created', 201, {'Location': resp.location})
def micropub_endpoint(): current_app.logger.info( "received micropub request %s, args=%s, form=%s, headers=%s", request, request.args, request.form, request.headers) bearer_prefix = 'Bearer ' header_token = request.headers.get('authorization') if header_token and header_token.startswith(bearer_prefix): token = header_token[len(bearer_prefix):] else: token = request.form.get('access_token') if not token: current_app.logger.warn('hit micropub endpoint with no access token') abort(401) try: decoded = util.jwt_decode(token) except jwt.DecodeError as e: current_app.logger.warn('could not decode access token: %s', e) abort(401) me = decoded.get('me') client_id = decoded.get('client_id') cred = Credential.query.filter_by(type='indieauth', value=me).first() user = cred and cred.user if not user or not user.is_authenticated(): current_app.logger.warn( 'received valid access token for invalid user: %s', me) abort(401) if request.method == 'GET': current_app.logger.debug('micropub GET request %s -> %s', request, request.args) accept_header = request.headers.get('accept', '') q = request.args.get('q') if q == 'syndicate-to': current_app.logger.debug('returning syndication targets') if 'application/json' in accept_header: return jsonify({ 'syndicate-to': util.trim_nulls([{ 'uid': t.uid, 'name': t.name, 'user': { 'name': t.user_name, 'url': t.user_url, 'photo': t.user_photo, }, 'service': { 'name': t.service_name, 'url': t.service_url, 'photo': t.service_photo, }, } for t in user.posse_targets]) }) else: response = make_response( urllib.parse.urlencode([('syndicate-to[]', t.uid) for t in user.posse_targets])) response.headers[ 'Content-Type'] = 'application/x-www-form-urlencoded' return response elif q in ('actions', 'json_actions'): current_app.logger.debug('returning action handlers') reply_url = url_for('admin.new_post', type='reply', _external=True) repost_url = url_for('admin.new_post', type='share', _external=True) like_url = url_for('admin.new_post', type='like', _external=True) payload = { 'reply': reply_url + '?url={url}', 'repost': repost_url + '?url={url}', 'favorite': like_url + '?url={url}', 'like': like_url + '?url={url}', } if q == 'json_actions' or 'application/json' in accept_header: return jsonify(payload) else: response = make_response(urllib.parse.urlencode(payload)) response.headers[ 'Content-Type'] = 'application/x-www-form-urlencoded' return response else: abort(404) h = request.form.get('h') in_reply_to = request.form.get('in-reply-to') like_of = request.form.get('like-of') photo_url = request.form.get('photo') photo_file = request.files.get('photo') bookmark = request.form.get('bookmark') or request.form.get('bookmark-of') repost_of = request.form.get('repost-of') post_type = ('event' if h == 'event' else 'article' if 'name' in request.form else 'photo' if photo_file or photo_url else 'reply' if in_reply_to else 'like' if like_of else 'bookmark' if bookmark else 'share' if repost_of else 'note') latitude = None longitude = None location_name = None venue_id = None loc_str = request.form.get('location') geo_prefix = 'geo:' if loc_str: if loc_str.startswith(geo_prefix): loc_str = loc_str[len(geo_prefix):] loc_params = loc_str.split(';') if loc_params: latitude, longitude = loc_params[0].split(',', 1) location_name = request.form.get('place_name') else: venue_prefix = urllib.parse.urljoin(get_settings().site_url, 'venues/') if loc_str.startswith(venue_prefix): slug = loc_str[len(venue_prefix):] venue = Venue.query.filter_by(slug=slug).first() if venue: venue_id = venue.id # url of the venue, e.g. https://kylewm.com/venues/cafe-trieste-berkeley-california venue = request.form.get('venue') syndicate_to = request.form.getlist('syndicate-to[]') syndication = request.form.getlist('syndication[]') if syndication: syndication = '\n'.join(syndication) else: syndication = request.form.get('syndication') # TODO check client_id if syndication: current_app.logger.debug( 'checking for existing post with syndication %s', syndication) existing = Post.query.filter( Post.syndication.like(db.literal('%"' + syndication + '"%')), ~Post.deleted).first() if existing: current_app.logger.debug('found post for %s: %s', syndication, existing) return redirect(existing.permalink) else: current_app.logger.debug('no post found with syndication %s', syndication) # translate from micropub's verbage.TODO unify translated = util.trim_nulls({ 'post_type': post_type, 'published': request.form.get('published'), 'start': request.form.get('start'), 'end': request.form.get('end'), 'title': request.form.get('name'), 'content': request.form.get('content[html]') or request.form.get('content'), 'venue': venue_id, 'latitude': latitude, 'longitude': longitude, 'location_name': location_name, 'syndication': syndication, 'in_reply_to': in_reply_to, 'like_of': like_of, 'repost_of': repost_of, 'bookmark_of': bookmark, 'photo': photo_file or photo_url, 'syndicate-to': syndicate_to, 'hidden': 'true' if like_of or bookmark else 'false', }) with current_app.test_request_context(base_url=get_settings().site_url, path='/save_new', method='POST', data=translated): current_app.logger.debug('received fake request %s: %s', request, request.args) login_user(user) current_app.logger.debug('successfully authenticated as user %s => %s', me, user) from . import admin resp = admin.save_new() return make_response('Created', 201, {'Location': resp.location})
def save_post(post): was_draft = post.draft pub_str = request.form.get('published') if pub_str: post.published = mf2util.parse_dt(pub_str) if post.published.tzinfo: post.published = post.published.astimezone(datetime.timezone.utc)\ .replace(tzinfo=None) if 'post_type' in request.form: post.post_type = request.form.get('post_type') start_str = request.form.get('start') if start_str: start = mf2util.parse_dt(start_str) if start: post.start = start post.start_utcoffset = start.utcoffset() end_str = request.form.get('end') if end_str: end = mf2util.parse_dt(end_str) if end: post.end = end post.end_utcoffset = end.utcoffset() now = datetime.datetime.utcnow() if not post.published or was_draft: post.published = now post.updated = now # populate the Post object and save it to the database, # redirect to the view post.title = request.form.get('title', '') post.content = request.form.get('content') post.draft = request.form.get('action') == 'save_draft' post.hidden = request.form.get('hidden', 'false') == 'true' post.friends_only = request.form.get('friends_only', 'false') == 'true' venue_name = request.form.get('new_venue_name') venue_lat = request.form.get('new_venue_latitude') venue_lng = request.form.get('new_venue_longitude') if venue_name and venue_lat and venue_lng: venue = Venue() venue.name = venue_name venue.location = { 'latitude': float(venue_lat), 'longitude': float(venue_lng), } venue.update_slug('{}-{}'.format(venue_lat, venue_lng)) db.session.add(venue) db.session.commit() hooks.fire('venue-saved', venue, request.form) post.venue = venue else: venue_id = request.form.get('venue') if venue_id: post.venue = Venue.query.get(venue_id) lat = request.form.get('latitude') lon = request.form.get('longitude') if lat and lon: if post.location is None: post.location = {} post.location['latitude'] = float(lat) post.location['longitude'] = float(lon) loc_name = request.form.get('location_name') if loc_name is not None: post.location['name'] = loc_name else: post.location = None for url_attr, context_attr in (('in_reply_to', 'reply_contexts'), ('repost_of', 'repost_contexts'), ('like_of', 'like_contexts'), ('bookmark_of', 'bookmark_contexts')): url_str = request.form.get(url_attr) if url_str is not None: urls = util.multiline_string_to_list(url_str) setattr(post, url_attr, urls) # fetch contexts before generating a slug contexts.fetch_contexts(post) if 'item-name' in request.form: post.item = util.trim_nulls({ 'name': request.form.get('item-name'), 'author': request.form.get('item-author'), 'photo': request.form.get('item-photo'), }) if 'rating' in request.form: rating = request.form.get('rating') post.rating = int(rating) if rating else None syndication = request.form.get('syndication') if syndication is not None: post.syndication = util.multiline_string_to_list(syndication) audience = request.form.get('audience') if audience is not None: post.audience = util.multiline_string_to_list(audience) tags = request.form.getlist('tags') if post.post_type != 'article' and post.content: # parse out hashtags as tag links from note-like posts tags += util.find_hashtags(post.content) tags = list(filter(None, map(util.normalize_tag, tags))) post.tags = [Tag.query.filter_by(name=tag).first() or Tag(tag) for tag in tags] post.people = [] people = request.form.getlist('people') for person in people: nick = Nick.query.filter_by(name=person).first() if nick: post.people.append(nick.contact) slug = request.form.get('slug') if slug: post.slug = util.slugify(slug) elif not post.slug or was_draft: post.slug = post.generate_slug() # events should use their start date for permalinks path_date = post.start or post.published if post.draft: m = hashlib.md5() m.update(bytes(path_date.isoformat() + '|' + post.slug, 'utf-8')) post.path = 'drafts/{}'.format(m.hexdigest()) elif not post.path or was_draft: base_path = '{}/{:02d}/{}'.format( path_date.year, path_date.month, post.slug) # generate a unique path unique_path = base_path idx = 1 while Post.load_by_path(unique_path): unique_path = '{}-{}'.format(base_path, idx) idx += 1 post.path = unique_path # generate short path if not post.short_path: short_base = '{}/{}'.format( util.tag_for_post_type(post.post_type), util.base60_encode(util.date_to_ordinal(path_date))) short_paths = set( row[0] for row in db.session.query(Post.short_path).filter( Post.short_path.startswith(short_base)).all()) for idx in itertools.count(1): post.short_path = short_base + util.base60_encode(idx) if post.short_path not in short_paths: break infiles = request.files.getlist('files') + request.files.getlist('photo') current_app.logger.debug('infiles: %s', infiles) for infile in infiles: if infile and infile.filename: current_app.logger.debug('receiving uploaded file %s', infile) attachment = create_attachment_from_file(post, infile) os.makedirs(os.path.dirname(attachment.disk_path), exist_ok=True) infile.save(attachment.disk_path) post.attachments.append(attachment) photo_url = request.form.get('photo') if photo_url: current_app.logger.debug('downloading photo from url %s', photo_url) temp_filename, headers = urllib.request.urlretrieve(photo_url) content_type = headers.get('content-type', '') mimetype = content_type and content_type.split(';')[0].strip() filename = os.path.basename(urllib.parse.urlparse(photo_url).path) attachment = create_attachment(post, filename, mimetype) os.makedirs(os.path.dirname(attachment.disk_path), exist_ok=True) shutil.copyfile(temp_filename, attachment.disk_path) urllib.request.urlcleanup() post.attachments.append(attachment) # pre-render the post html html = util.markdown_filter(post.content, img_path=post.get_image_path()) html = util.autolink(html) if post.post_type == 'article': html = util.process_people_to_microcards(html) else: html = util.process_people_to_at_names(html) post.content_html = html if not post.id: db.session.add(post) db.session.commit() current_app.logger.debug('saved post %d %s', post.id, post.permalink) redirect_url = post.permalink hooks.fire('post-saved', post, request.form) return redirect(redirect_url)