add likes + reposts to post types
This commit is contained in:
parent
fe0e9c07f9
commit
2a70980410
231
example.py
231
example.py
|
@ -1,22 +1,29 @@
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Optional
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import functools
|
import functools
|
||||||
import dotenv
|
import dotenv
|
||||||
import giteapy
|
import giteapy
|
||||||
|
import giteapy.rest
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
import base64
|
import base64
|
||||||
from werkzeug.datastructures import FileStorage
|
from werkzeug.datastructures import FileStorage
|
||||||
import yaml
|
import yaml
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
from flask import Flask, request, url_for, Response
|
from flask import Flask, request, url_for, Response
|
||||||
from requests import api
|
from requests import api
|
||||||
from flask_micropub import MicropubClient
|
from flask_micropub import MicropubClient
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
PERMITTED_DOMAIN = os.environ.get(
|
PERMITTED_DOMAIN = os.environ.get(
|
||||||
|
@ -50,8 +57,8 @@ def authed_endpoint(f):
|
||||||
auth = requests.get("https://tokens.indieauth.com/token", headers={
|
auth = requests.get("https://tokens.indieauth.com/token", headers={
|
||||||
"Authorization": authtok, "Accept": "application/json"}).json()
|
"Authorization": authtok, "Accept": "application/json"}).json()
|
||||||
|
|
||||||
if auth['me'] not in PERMITTED_DOMAIN:
|
if auth.get('me','') not in PERMITTED_DOMAIN:
|
||||||
return {"error": "forbidden", "error_description": f"User {auth['me']} not permitted to post here"}, 403
|
return {"error": "insufficient_scope", "error_description": f"User \"{auth.get('me','')}\" not permitted to post here"}, 401
|
||||||
|
|
||||||
return f(*args, *kwargs)
|
return f(*args, *kwargs)
|
||||||
|
|
||||||
|
@ -61,7 +68,7 @@ def authed_endpoint(f):
|
||||||
_api_client = None
|
_api_client = None
|
||||||
|
|
||||||
|
|
||||||
def get_api_client():
|
def get_api_client() -> giteapy.RepositoryApi:
|
||||||
global _api_client
|
global _api_client
|
||||||
|
|
||||||
if _api_client is None:
|
if _api_client is None:
|
||||||
|
@ -96,7 +103,6 @@ def process_photo_url(now: datetime, doc: Dict[str, List[str]], suffix: str = ""
|
||||||
with open(filename, 'wb') as f:
|
with open(filename, 'wb') as f:
|
||||||
f.write(r.content)
|
f.write(r.content)
|
||||||
|
|
||||||
# elif os.environ.get('MICROPUB_IMAGE_STRATEGY') == 'gitea':
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
photo_url = doc['photo']
|
photo_url = doc['photo']
|
||||||
|
@ -130,14 +136,143 @@ def process_photo_upload(now: datetime, file: FileStorage, suffix: str=""):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def init_frontmatter(now: datetime, post_type: str, name: Optional[str]=None):
|
||||||
|
|
||||||
@app.route('/', methods=['POST'])
|
now_ts = int(time.mktime(now.timetuple()))
|
||||||
@authed_endpoint
|
|
||||||
def req():
|
|
||||||
|
|
||||||
|
if name:
|
||||||
|
slug = slugify(name) + str(now_ts)
|
||||||
|
else:
|
||||||
|
slug = str(now_ts)
|
||||||
|
|
||||||
|
|
||||||
|
url = os.path.join("/", ENTITY_TYPE_PLURAL_MAP.get(post_type, post_type + "s"),
|
||||||
|
now.strftime("%Y/%m/%d"), slug)
|
||||||
|
|
||||||
|
|
||||||
|
file_path = os.path.join(os.environ.get(
|
||||||
|
'CONTENT_PREFIX'), ENTITY_TYPE_PLURAL_MAP.get(post_type, post_type + "s"), now.strftime("%Y/%m/%d"), slug + ".md")
|
||||||
|
|
||||||
|
frontmatter = {
|
||||||
|
"url": url,
|
||||||
|
"type": post_type,
|
||||||
|
"date": now.isoformat(sep='T'),
|
||||||
|
}
|
||||||
|
|
||||||
|
return frontmatter, file_path
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/webmentions", methods=['POST'])
|
||||||
|
def webmention_hook():
|
||||||
|
"""Accept web mention webhook request"""
|
||||||
|
|
||||||
|
|
||||||
|
body = request.get_json()
|
||||||
|
|
||||||
|
print(f"Incoming webmention {body}")
|
||||||
|
|
||||||
|
# webmention should always have a json body
|
||||||
|
if body is None:
|
||||||
|
return {"error":"invalid_request"}, 400
|
||||||
|
|
||||||
|
# assert that secret matches
|
||||||
|
if body.get('secret') != os.environ.get("WEBMENTION_SECRET", "changeme"):
|
||||||
|
return {"error":"invalid_secret"}, 401
|
||||||
|
|
||||||
|
# get existing mentions from gitea
|
||||||
|
|
||||||
|
api = get_api_client()
|
||||||
|
|
||||||
|
old_sha = None
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
mentions_meta = api.repo_get_contents(os.environ.get('GITEA_REPO_OWNER'),
|
||||||
|
os.environ.get('GITEA_REPO_NAME'), os.environ.get('WEBMENTIONS_JSON_FILE'))
|
||||||
|
|
||||||
|
old_sha = mentions_meta.sha
|
||||||
|
|
||||||
|
mentions = json.loads(base64.decodebytes(mentions_meta.content.encode('utf8')))
|
||||||
|
|
||||||
|
except giteapy.rest.ApiException as e:
|
||||||
|
if e.status == 404:
|
||||||
|
print("no mentions yet, create new mentions file")
|
||||||
|
mentions = {}
|
||||||
|
|
||||||
|
# parse target url and get path on server
|
||||||
|
url_path = urlparse(body.get('target')).path
|
||||||
|
|
||||||
|
# create mention entry if needed
|
||||||
|
if url_path not in mentions:
|
||||||
|
mentions[url_path] = []
|
||||||
|
|
||||||
|
# if the mention is already processed, do nothing.
|
||||||
|
for entry in mentions[url_path]:
|
||||||
|
if entry['source'] == body['source']:
|
||||||
|
return {"message": "mention already processed, no action taken."}
|
||||||
|
|
||||||
|
activity_types = {
|
||||||
|
"in-reply-to": "reply",
|
||||||
|
"like-of": "like",
|
||||||
|
"repost-of": "repost",
|
||||||
|
"bookmark-of": "bookmark",
|
||||||
|
"mention-of": "mention",
|
||||||
|
"rsvp":"rsvp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# format the mention like the data from webmention.io api
|
||||||
|
new_mention = {
|
||||||
|
"id": body['post']['wm-id'],
|
||||||
|
"source": body['source'],
|
||||||
|
"target": body['target'],
|
||||||
|
"activity":{
|
||||||
|
"type": activity_types[body['post']['wm-property']]
|
||||||
|
},
|
||||||
|
"verified_date": body.get('wm-received', datetime.now().isoformat()),
|
||||||
|
"data":{
|
||||||
|
"author": body['post']['author'],
|
||||||
|
"content": body['post'].get('content',{}).get('html', None),
|
||||||
|
"published": body['post'].get('published')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# append the new mention
|
||||||
|
mentions[url_path].append(new_mention)
|
||||||
|
|
||||||
|
content = base64.encodestring(json.dumps(mentions, indent=2).encode("utf8")).decode("utf8")
|
||||||
|
|
||||||
|
# store to repo
|
||||||
|
if old_sha:
|
||||||
|
print(f"Update {os.environ.get('WEBMENTIONS_JSON_FILE')}")
|
||||||
|
body = giteapy.UpdateFileOptions(content=content, sha=old_sha)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = api.repo_update_file(os.environ.get(
|
||||||
|
'GITEA_REPO_OWNER'), os.environ.get('GITEA_REPO_NAME'), os.environ.get('WEBMENTIONS_JSON_FILE'), body)
|
||||||
|
|
||||||
|
return {"message":"ok"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Create {os.environ.get('WEBMENTIONS_JSON_FILE')}")
|
||||||
|
body = giteapy.CreateFileOptions(content=content)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = api.repo_create_file(os.environ.get(
|
||||||
|
'GITEA_REPO_OWNER'), os.environ.get('GITEA_REPO_NAME'), os.environ.get('WEBMENTIONS_JSON_FILE'), body)
|
||||||
|
|
||||||
|
return {"message":"ok"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
|
|
||||||
|
def process_multipart_post():
|
||||||
doc = request.form.to_dict(flat=True)
|
doc = request.form.to_dict(flat=True)
|
||||||
|
|
||||||
|
|
||||||
if 'in-reply-to' in doc:
|
if 'in-reply-to' in doc:
|
||||||
entry_type = "reply"
|
entry_type = "reply"
|
||||||
|
|
||||||
|
@ -148,34 +283,16 @@ def req():
|
||||||
elif 'like-of' in doc:
|
elif 'like-of' in doc:
|
||||||
entry_type = "like"
|
entry_type = "like"
|
||||||
|
|
||||||
|
|
||||||
# elif ('photo' in doc) or ('photo' in request.files) or ('photo[]' in request.files):
|
|
||||||
# entry_type = "photo"
|
|
||||||
|
|
||||||
elif 'name' in doc:
|
elif 'name' in doc:
|
||||||
entry_type = "post"
|
entry_type = "post"
|
||||||
else:
|
else:
|
||||||
entry_type = "note"
|
entry_type = "note"
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
now_ts = int(time.mktime(now.timetuple()))
|
|
||||||
|
|
||||||
url = os.path.join("/", ENTITY_TYPE_PLURAL_MAP.get(entry_type, entry_type + "s"),
|
frontmatter, file_path = init_frontmatter(now, entry_type, doc.get('name'))
|
||||||
now.strftime("%Y/%m/%d"), str(now_ts))
|
|
||||||
|
|
||||||
if 'name' in doc:
|
|
||||||
slug = slugify(doc['name']) + str(now_ts)
|
|
||||||
else:
|
|
||||||
slug = str(now_ts)
|
|
||||||
|
|
||||||
file_path = os.path.join(os.environ.get(
|
|
||||||
'CONTENT_PREFIX'), ENTITY_TYPE_PLURAL_MAP.get(entry_type, entry_type + "s"), now.strftime("%Y/%m/%d"), slug + ".md")
|
|
||||||
|
|
||||||
frontmatter = {
|
|
||||||
"url": url,
|
|
||||||
"type": entry_type,
|
|
||||||
"date": now.isoformat(sep='T'),
|
|
||||||
}
|
|
||||||
|
|
||||||
for key in ['bookmark-of', 'in-reply-to', 'repost-of', 'like-of']:
|
for key in ['bookmark-of', 'in-reply-to', 'repost-of', 'like-of']:
|
||||||
if key in doc:
|
if key in doc:
|
||||||
|
@ -205,7 +322,7 @@ def req():
|
||||||
for i, photo in enumerate(photos):
|
for i, photo in enumerate(photos):
|
||||||
photo_url = process_photo_upload(now, photo, suffix=i)
|
photo_url = process_photo_upload(now, photo, suffix=i)
|
||||||
|
|
||||||
frontmatter.photo.append(photo_url)
|
frontmatter['photo'].append(photo_url)
|
||||||
|
|
||||||
docstr += f"\n\n<img src=\"{photo_url}\" class=\"u-photo\" />"
|
docstr += f"\n\n<img src=\"{photo_url}\" class=\"u-photo\" />"
|
||||||
|
|
||||||
|
@ -223,18 +340,40 @@ def req():
|
||||||
|
|
||||||
docstr = f"<img src=\"{photo_url}\" class=\"u-photo\" /> \n\n {doc['content']}"
|
docstr = f"<img src=\"{photo_url}\" class=\"u-photo\" /> \n\n {doc['content']}"
|
||||||
else:
|
else:
|
||||||
docstr = doc['content']
|
docstr = doc.get('content','') if 'content' in doc else ""
|
||||||
|
|
||||||
if 'mp-syndicate-to' in doc:
|
if 'mp-syndicate-to' in doc:
|
||||||
|
frontmatter['mp-syndicate-to'] = doc['mp-syndicate-to'].split(",")
|
||||||
|
|
||||||
for url in doc['mp-syndicate-to'].split(","):
|
for url in doc['mp-syndicate-to'].split(","):
|
||||||
docstr += f"\n<a href=\"{url}\"></a>"
|
docstr += f"\n<a href=\"{url}\"></a>"
|
||||||
|
|
||||||
|
if 'mp-syndicate-to[]' in request.form:
|
||||||
|
frontmatter['mp-syndicate-to'] = request.form.getlist('mp-syndicate-to[]')
|
||||||
|
|
||||||
|
for url in request.form.getlist('mp-syndicate-to[]'):
|
||||||
|
docstr += f"\n<a href=\"{url}\"></a>"
|
||||||
|
|
||||||
|
return docstr, frontmatter, file_path
|
||||||
|
|
||||||
|
def process_json_post():
|
||||||
|
"""Process JSON POST submission"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/', methods=['POST'])
|
||||||
|
@authed_endpoint
|
||||||
|
def req():
|
||||||
|
|
||||||
|
if request.get_json():
|
||||||
|
docstr, frontmatter, file_path = process_json_post()
|
||||||
|
else:
|
||||||
|
docstr, frontmatter, file_path = process_multipart_post()
|
||||||
|
|
||||||
frontmatter_str = yaml.dump(frontmatter)
|
frontmatter_str = yaml.dump(frontmatter)
|
||||||
content = base64.encodestring(
|
content = base64.encodestring(
|
||||||
f"---\n{frontmatter_str}\n---\n\n{docstr}".encode("utf8")).decode("utf8")
|
f"---\n{frontmatter_str}\n---\n\n{docstr}".encode("utf8")).decode("utf8")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
api = get_api_client()
|
api = get_api_client()
|
||||||
|
|
||||||
body = giteapy.CreateFileOptions(content=content)
|
body = giteapy.CreateFileOptions(content=content)
|
||||||
|
@ -243,7 +382,7 @@ def req():
|
||||||
r = api.repo_create_file(os.environ.get(
|
r = api.repo_create_file(os.environ.get(
|
||||||
'GITEA_REPO_OWNER'), os.environ.get('GITEA_REPO_NAME'), file_path, body)
|
'GITEA_REPO_OWNER'), os.environ.get('GITEA_REPO_NAME'), file_path, body)
|
||||||
|
|
||||||
return Response(status=202, headers={"Location": url})
|
return Response(status=202, headers={"Location": frontmatter['url']})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}, 500
|
return {"error": str(e)}, 500
|
||||||
|
@ -282,6 +421,15 @@ def get_syndication_targets():
|
||||||
return defs
|
return defs
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/media", methods=["POST"])
|
||||||
|
@authed_endpoint
|
||||||
|
def media_endpoint():
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
url = process_photo_upload(now, request.files['file'])
|
||||||
|
|
||||||
|
return Response(status=201, headers={"Location": url})
|
||||||
|
|
||||||
|
|
||||||
def generate_config_json():
|
def generate_config_json():
|
||||||
|
|
||||||
|
@ -308,6 +456,18 @@ def generate_config_json():
|
||||||
{
|
{
|
||||||
"type": "bookmark",
|
"type": "bookmark",
|
||||||
"name": "Bookmark"
|
"name": "Bookmark"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type":"like",
|
||||||
|
"name":"Like"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type":"repost",
|
||||||
|
"name":"Re-post"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type":"read",
|
||||||
|
"name":"Read"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -322,6 +482,9 @@ def index():
|
||||||
|
|
||||||
elif request.args.get('q') == 'category':
|
elif request.args.get('q') == 'category':
|
||||||
return parse_categories()
|
return parse_categories()
|
||||||
|
|
||||||
|
elif request.args.get('q') == 'syndicate-to':
|
||||||
|
return {"syndicate-to": get_syndication_targets()}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/form', methods=['GET'])
|
@app.route('/form', methods=['GET'])
|
||||||
|
|
Loading…
Reference in New Issue