listssrht: Use GraphQL for list CRUD operations
This commit is contained in:
parent
17d52d7b3f
commit
2ae65a9e81
|
@ -1,4 +1,4 @@
|
|||
from flask import Blueprint, abort, request
|
||||
from flask import current_app, Blueprint, abort, request
|
||||
from listssrht.blueprints.api import get_user, get_list
|
||||
from listssrht.blueprints.archives import apply_search
|
||||
from listssrht.types import List, Email, ListAccess, Subscription
|
||||
|
@ -6,6 +6,7 @@ from listssrht.webhooks import ListWebhook, UserWebhook
|
|||
from sqlalchemy import or_
|
||||
from srht.api import paginated_response
|
||||
from srht.database import db
|
||||
from srht.graphql import exec_gql
|
||||
from srht.oauth import oauth, current_token
|
||||
from srht.validation import Validation
|
||||
|
||||
|
@ -28,23 +29,63 @@ def user_lists_GET(username):
|
|||
def user_lists_POST():
|
||||
user = current_token.user
|
||||
valid = Validation(request)
|
||||
ml = List(user, valid)
|
||||
name = valid.require("name", friendly_name="Name")
|
||||
description = valid.optional("description")
|
||||
if not valid.ok:
|
||||
return valid.response
|
||||
db.session.add(ml)
|
||||
db.session.flush()
|
||||
UserWebhook.deliver(UserWebhook.Events.list_create,
|
||||
ml.to_dict(), UserWebhook.Subscription.user_id == ml.owner_id)
|
||||
db.session.commit()
|
||||
|
||||
# Auto-subscribe the owner
|
||||
sub = Subscription()
|
||||
sub.user_id = user.id
|
||||
sub.list_id = ml.id
|
||||
db.session.add(sub)
|
||||
db.session.commit()
|
||||
resp = exec_gql(current_app.site, """
|
||||
mutation CreateMailingList($name: String!, $description: String) {
|
||||
createMailingList(name: $name, description: $description) {
|
||||
id
|
||||
name
|
||||
owner {
|
||||
canonical_name: canonicalName
|
||||
... on User {
|
||||
name: username
|
||||
}
|
||||
}
|
||||
created
|
||||
updated
|
||||
description
|
||||
nonsubscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
subscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
identified {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
}
|
||||
}
|
||||
""", user=user, valid=valid, name=name, description=description)
|
||||
|
||||
return ml.to_dict(), 201
|
||||
if not valid.ok:
|
||||
return valid.response
|
||||
|
||||
resp = resp["createMailingList"]
|
||||
|
||||
permList = lambda acl: [key for key in [
|
||||
"browse", "reply", "post"
|
||||
] if acl[key]]
|
||||
|
||||
resp["permissions"] = {
|
||||
"nonsubscriber": permList(resp["nonsubscriber"]),
|
||||
"subscriber": permList(resp["subscriber"]),
|
||||
"account": permList(resp["identified"]),
|
||||
}
|
||||
del resp["nonsubscriber"]
|
||||
del resp["subscriber"]
|
||||
del resp["identified"]
|
||||
|
||||
return resp, 201
|
||||
|
||||
@lists.route("/api/user/<username>/lists/<list_name>")
|
||||
@lists.route("/api/lists/<list_name>", defaults={"username": None})
|
||||
|
@ -78,12 +119,67 @@ def user_lists_by_name_PUT(list_name):
|
|||
user, ml, access = get_list(None, list_name)
|
||||
if ml.owner_id != user.id:
|
||||
abort(403)
|
||||
|
||||
valid = Validation(request)
|
||||
ml.update(valid)
|
||||
ListWebhook.deliver(ListWebhook.Events.list_update,
|
||||
ml.to_dict(), ListWebhook.Subscription.list_id == ml.id)
|
||||
db.session.commit()
|
||||
return ml.to_dict()
|
||||
rewrite = lambda value: None if value == "" else value
|
||||
input = {
|
||||
key: rewrite(valid.source[key]) for key in [
|
||||
"description"
|
||||
] if valid.source.get(key) is not None
|
||||
}
|
||||
|
||||
resp = exec_gql(current_app.site, """
|
||||
mutation UpdateMailingList($id: Int!, $input: MailingListInput!) {
|
||||
updateMailingList(id: $id, input: $input) {
|
||||
id
|
||||
name
|
||||
owner {
|
||||
canonical_name: canonicalName
|
||||
... on User {
|
||||
name: username
|
||||
}
|
||||
}
|
||||
created
|
||||
updated
|
||||
description
|
||||
nonsubscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
subscriber {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
identified {
|
||||
browse
|
||||
reply
|
||||
post
|
||||
}
|
||||
}
|
||||
}
|
||||
""", user=user, valid=valid, id=ml.id, input=input)
|
||||
|
||||
if not valid.ok:
|
||||
return valid.response
|
||||
|
||||
resp = resp["updateMailingList"]
|
||||
|
||||
permList = lambda acl: [key for key in [
|
||||
"browse", "reply", "post"
|
||||
] if acl[key]]
|
||||
|
||||
resp["permissions"] = {
|
||||
"nonsubscriber": permList(resp["nonsubscriber"]),
|
||||
"subscriber": permList(resp["subscriber"]),
|
||||
"account": permList(resp["identified"]),
|
||||
}
|
||||
del resp["nonsubscriber"]
|
||||
del resp["subscriber"]
|
||||
del resp["identified"]
|
||||
|
||||
return resp
|
||||
|
||||
@lists.route("/api/lists/<list_name>", methods=["DELETE"])
|
||||
@oauth("lists:write")
|
||||
|
@ -91,10 +187,11 @@ def user_lists_by_name_DELETE(list_name):
|
|||
user, ml, access = get_list(None, list_name)
|
||||
if ml.owner_id != user.id:
|
||||
abort(403)
|
||||
ListWebhook.deliver(ListWebhook.Events.list_delete,
|
||||
ml.to_dict(), ListWebhook.Subscription.list_id == ml.id)
|
||||
db.engine.execute(f"DELETE FROM list WHERE id = {ml.id};")
|
||||
db.session.commit()
|
||||
exec_gql(current_app.site, """
|
||||
mutation DeleteMailingList($id: Int!) {
|
||||
deleteMailingList(id: $id) { id }
|
||||
}
|
||||
""", user=user, id=ml.id)
|
||||
return {}, 204
|
||||
|
||||
@lists.route("/api/user/<username>/lists/<list_name>/posts")
|
||||
|
|
|
@ -3,6 +3,7 @@ from flask import current_app, send_file, session
|
|||
from srht.config import cfg
|
||||
from srht.database import db
|
||||
from srht.flask import paginate_query
|
||||
from srht.graphql import exec_gql
|
||||
from srht.oauth import current_user, loginrequired
|
||||
from srht.validation import Validation
|
||||
from listssrht.blueprints.archives import get_list
|
||||
|
@ -47,23 +48,27 @@ def info_POST(owner_name, list_name):
|
|||
abort(403)
|
||||
|
||||
valid = Validation(request)
|
||||
list_desc = valid.optional("list_desc")
|
||||
if list_desc == "":
|
||||
list_desc = None
|
||||
valid.expect(not list_desc or len(list_desc) < 2048,
|
||||
"Description must be between 16 and 2048 characters.",
|
||||
field="list_desc")
|
||||
rewrite = lambda value: None if value == "" else value
|
||||
input = {
|
||||
key: rewrite(valid.source[key]) for key in [
|
||||
"description"
|
||||
] if valid.source.get(key) is not None
|
||||
}
|
||||
|
||||
exec_gql(current_app.site, """
|
||||
mutation UpdateMailingList($id: Int!, $input: MailingListInput!) {
|
||||
updateMailingList(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
""", valid=valid, id=ml.id, input=input)
|
||||
|
||||
if not valid.ok:
|
||||
return render_template("settings-info.html", list=ml, owner=owner,
|
||||
access_type_list=ListAccess, access_help_map=access_help_map,
|
||||
view="info", **valid.kwargs)
|
||||
|
||||
ml.description = list_desc
|
||||
ListWebhook.deliver(ListWebhook.Events.list_update,
|
||||
ml.to_dict(), ListWebhook.Subscription.list_id == ml.id)
|
||||
db.session.commit()
|
||||
return redirect(url_for("archives.archive",
|
||||
return redirect(url_for("settings.info_GET",
|
||||
owner_name=owner_name, list_name=list_name))
|
||||
|
||||
@settings.route("/<owner_name>/<list_name>/settings/access")
|
||||
|
@ -98,19 +103,26 @@ def access_POST(owner_name, list_name):
|
|||
abort(403)
|
||||
|
||||
valid = Validation(request)
|
||||
access = _process_access(valid, "default")
|
||||
input = {
|
||||
perm: ((access & ListAccess[perm].value) != 0) for perm in [
|
||||
"browse", "reply", "post", "moderate",
|
||||
]
|
||||
}
|
||||
|
||||
ml.nonsubscriber_permissions = _process_access(valid, "nonsub")
|
||||
ml.subscriber_permissions = _process_access(valid, "sub")
|
||||
ml.account_permissions = _process_access(valid, "account")
|
||||
if ListAccess.browse in ml.nonsubscriber_permissions:
|
||||
ml.subscriber_permissions |= ListAccess.browse
|
||||
ml.account_permissions |= ListAccess.browse
|
||||
ml.subscriber_permissions |= ml.nonsubscriber_permissions
|
||||
ml.account_permissions |= ml.nonsubscriber_permissions
|
||||
exec_gql(current_app.site, """
|
||||
mutation UpdateMailingListACL($id: Int!, $input: ACLInput!) {
|
||||
updateMailingListACL(listID: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
""", valid=valid, id=ml.id, input=input)
|
||||
|
||||
if not valid.ok:
|
||||
return render_template("settings-access.html", view="access",
|
||||
ml=ml, owner=owner, access_type_list=ListAccess,
|
||||
access_help_map=access_help_map, **valid.kwargs)
|
||||
|
||||
ListWebhook.deliver(ListWebhook.Events.list_update,
|
||||
ml.to_dict(), ListWebhook.Subscription.list_id == ml.id)
|
||||
db.session.commit()
|
||||
return redirect(url_for("settings.access_GET",
|
||||
owner_name=owner_name, list_name=list_name))
|
||||
|
||||
|
@ -216,10 +228,27 @@ def content_POST(owner_name, list_name):
|
|||
abort(403)
|
||||
|
||||
valid = Validation(request)
|
||||
ml.permit_mimetypes = valid.optional("permit_mimetypes")
|
||||
ml.reject_mimetypes = valid.optional("reject_mimetypes")
|
||||
rewrite = lambda value: [] if value == "" else value.split(",")
|
||||
input = {
|
||||
key: rewrite(valid.source[key]) for key in [
|
||||
"permitMime", "rejectMime"
|
||||
] if valid.source.get(key) is not None
|
||||
}
|
||||
|
||||
exec_gql(current_app.site, """
|
||||
mutation UpdateMailingList($id: Int!, $input: MailingListInput!) {
|
||||
updateMailingList(id: $id, input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
""", valid=valid, id=ml.id, input=input)
|
||||
|
||||
if not valid.ok:
|
||||
return render_template("settings-content.html",
|
||||
view="content", ml=ml, owner=owner,
|
||||
always_reject=list(filter(None, cfg("lists.sr.ht::worker", "reject-mimetypes").split(","))),
|
||||
**valid.kwargs)
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for("settings.content_GET",
|
||||
owner_name=owner_name, list_name=list_name))
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from email.mime.text import MIMEText
|
||||
from email.utils import parseaddr, formatdate, make_msgid
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, abort
|
||||
from flask import current_app, Blueprint, render_template, request, redirect, url_for, abort
|
||||
from flask import session
|
||||
from srht.config import cfg, cfgi
|
||||
from srht.database import db
|
||||
from srht.oauth import UserType, current_user, loginrequired
|
||||
from srht.flask import paginate_query
|
||||
from srht.graphql import exec_gql
|
||||
from srht.search import search_by
|
||||
from srht.validation import Validation
|
||||
from sqlalchemy import or_
|
||||
|
@ -108,24 +109,29 @@ def create_list_POST():
|
|||
abort(401)
|
||||
|
||||
valid = Validation(request)
|
||||
ml = List(current_user, valid)
|
||||
name = valid.require("name", friendly_name="Name")
|
||||
description = valid.optional("description")
|
||||
if not valid.ok:
|
||||
return render_template("create.html", **valid.kwargs)
|
||||
db.session.add(ml)
|
||||
db.session.flush()
|
||||
UserWebhook.deliver(UserWebhook.Events.list_create,
|
||||
ml.to_dict(), UserWebhook.Subscription.user_id == ml.owner_id)
|
||||
|
||||
# Auto-subscribe the owner
|
||||
sub = Subscription()
|
||||
sub.user_id = current_user.id
|
||||
sub.list_id = ml.id
|
||||
db.session.add(sub)
|
||||
db.session.commit()
|
||||
resp = exec_gql(current_app.site, """
|
||||
mutation CreateMailingList($name: String!, $description: String) {
|
||||
createMailingList(name: $name, description: $description) {
|
||||
name
|
||||
owner {
|
||||
canonicalName
|
||||
}
|
||||
}
|
||||
}
|
||||
""", valid=valid, name=name, description=description)
|
||||
|
||||
if not valid.ok:
|
||||
return render_template("create.html", **valid.kwargs)
|
||||
|
||||
resp = resp["createMailingList"]
|
||||
return redirect(url_for("archives.archive",
|
||||
owner_name=current_user.canonical_name,
|
||||
list_name=ml.name))
|
||||
owner_name=resp["owner"]["canonicalName"],
|
||||
list_name=resp["name"]))
|
||||
|
||||
@user.route("/lists/create-mirror")
|
||||
@loginrequired
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from srht.config import cfg, cfgi
|
||||
from srht.database import DbSession, db
|
||||
from srht.graphql import exec_gql
|
||||
if not hasattr(db, "session"):
|
||||
db = DbSession(cfg("lists.sr.ht", "connection-string"))
|
||||
import listssrht.types
|
||||
|
@ -707,9 +708,9 @@ def forward_thread(list_id, thread_id, recipient):
|
|||
|
||||
@task
|
||||
def delete_list(list_id):
|
||||
from listssrht.webhooks import ListWebhook
|
||||
ml = List.query.filter(List.id == list_id).one_or_none()
|
||||
ListWebhook.deliver(ListWebhook.Events.list_delete,
|
||||
ml.to_dict(), ListWebhook.Subscription.list_id == ml.id)
|
||||
db.engine.execute(f"DELETE FROM list WHERE id = {ml.id};")
|
||||
db.session.commit()
|
||||
exec_gql("lists.sr.ht", """
|
||||
mutation DeleteMailingList($id: Int!) {
|
||||
deleteMailingList(id: $id) { id }
|
||||
}
|
||||
""", user=ml.owner, id=ml.id)
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
>{{description or ""}}</textarea>
|
||||
{{valid.summary("description")}}
|
||||
</div>
|
||||
{{valid.summary()}}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Create {{icon("caret-right")}}
|
||||
</button>
|
||||
|
|
|
@ -55,37 +55,15 @@
|
|||
</p>
|
||||
<div class="event-list">
|
||||
<div class="event">
|
||||
<h4>Non-subscriber Permissions</h4>
|
||||
<h4>Default Permissions</h4>
|
||||
<p>
|
||||
Permissions granted to users who are not subscribed or logged
|
||||
in to a sr.ht account. Any permission granted to anonymous
|
||||
users are granted to subscribers and account holders as well,
|
||||
unless explicitly revoked from that user.
|
||||
These permissions are used for anyone who does not have a more
|
||||
specific access configuration.
|
||||
</p>
|
||||
{% for a in access_type_list %}
|
||||
{{ perm_checkbox(a, ml.nonsubscriber_permissions , "nonsub") }}
|
||||
{{ perm_checkbox(a, ml.nonsubscriber_permissions , "default") }}
|
||||
{% endfor %}
|
||||
{{ valid.summary("list_nonsubscriber_access") }}
|
||||
</div>
|
||||
<div class="event">
|
||||
<h4>Subscriber Permissions</h4>
|
||||
<p>
|
||||
Permissions granted to users who are subscribed to the list.
|
||||
</p>
|
||||
{% for a in access_type_list %}
|
||||
{{ perm_checkbox(a, ml.subscriber_permissions , "sub") }}
|
||||
{% endfor %}
|
||||
{{ valid.summary("list_subscriber_access") }}
|
||||
</div>
|
||||
<div class="event">
|
||||
<h4>Account Holder Permissions</h4>
|
||||
<p>
|
||||
Permissions granted to logged in holders of sr.ht accounts.
|
||||
</p>
|
||||
{% for a in access_type_list %}
|
||||
{{ perm_checkbox(a, ml.account_permissions, "account") }}
|
||||
{% endfor %}
|
||||
{{ valid.summary("list_account_access") }}
|
||||
{{ valid.summary("list_default_access") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,18 +11,18 @@
|
|||
<form method="POST">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group">
|
||||
<label for="permit_mimetypes">
|
||||
<label for="permitMime">
|
||||
Permitted mimetypes
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="permit_mimetypes"
|
||||
id="permit_mimetypes"
|
||||
name="permitMime"
|
||||
id="permitMime"
|
||||
class="form-control"
|
||||
value="{{ ml.permit_mimetypes }}"
|
||||
aria-describedby="permit_mimetypes-help" />
|
||||
aria-describedby="permitMime-help" />
|
||||
<p
|
||||
id="permit_mimetypes-help"
|
||||
id="permitMime-help"
|
||||
class="form-text text-muted"
|
||||
>
|
||||
Comma separated list of mimetypes, <a
|
||||
|
@ -31,21 +31,21 @@
|
|||
target="_blank"
|
||||
>fnmatch</a> for wildcards.
|
||||
</p>
|
||||
{{ valid.summary("permit_mimetypes") }}
|
||||
{{ valid.summary("permitMime") }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reject_mimetypes">
|
||||
<label for="rejectMime">
|
||||
Rejected mimetypes
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="reject_mimetypes"
|
||||
id="reject_mimetypes"
|
||||
name="rejectMime"
|
||||
id="rejectMime"
|
||||
class="form-control"
|
||||
value="{{ ml.reject_mimetypes }}"
|
||||
aria-describedby="reject_mimetypes-help" />
|
||||
aria-describedby="rejectMime-help" />
|
||||
<p
|
||||
id="reject_mimetypes-help"
|
||||
id="rejectMime-help"
|
||||
class="form-text text-muted"
|
||||
>
|
||||
Comma separated list of mimetypes, <a
|
||||
|
@ -66,7 +66,7 @@
|
|||
always rejected.
|
||||
{% endif %}
|
||||
</p>
|
||||
{{ valid.summary("reject_mimetypes") }}
|
||||
{{ valid.summary("rejectMime") }}
|
||||
</div>
|
||||
{{ valid.summary() }}
|
||||
<span class="pull-right">
|
||||
|
|
|
@ -11,29 +11,29 @@
|
|||
<form method="POST">
|
||||
{{csrf_token()}}
|
||||
<div class="form-group">
|
||||
<label for="list_name">
|
||||
<label for="name">
|
||||
Name
|
||||
<span class="text-muted">(you can't edit this)</p>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="list_name"
|
||||
id="list_name"
|
||||
name="name"
|
||||
id="name"
|
||||
class="form-control"
|
||||
value="{{ ml.name }}"
|
||||
disabled />
|
||||
</div>
|
||||
<div class="form-group {{valid.cls("list_desc")}}">
|
||||
<label for="list_desc">Description</label>
|
||||
<div class="form-group {{valid.cls("description")}}">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
name="list_desc"
|
||||
id="list_desc"
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control"
|
||||
placeholder="Markdown supported"
|
||||
rows="10"
|
||||
aria-describedby="list_desc-help"
|
||||
>{{list_desc or ml.description or ""}}</textarea>
|
||||
{{ valid.summary("list_desc") }}
|
||||
aria-describedby="description-help"
|
||||
>{{description or ml.description or ""}}</textarea>
|
||||
{{ valid.summary("description") }}
|
||||
</div>
|
||||
{{ valid.summary() }}
|
||||
<span class="pull-right">
|
||||
|
|
Loading…
Reference in New Issue