Example #1
0
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
Example #2
0
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
Example #3
0
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()
Example #4
0
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})
Example #5
0
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})
Example #6
0
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)