todosrht-lmtp: Use GraphQL for ticket submission

Use GraphQL for ticket submission so that GraphQL TICKET_CREATED webhook
events are delivered.
This commit is contained in:
Adnan Maolood 2022-04-05 12:13:55 -04:00 committed by Drew DeVault
parent ebb55fc95a
commit dee9e75762
4 changed files with 147 additions and 19 deletions

View File

@ -14,6 +14,12 @@ access token, and are not available to clients using OAuth 2.0 access tokens.
"""
directive @private on FIELD_DEFINITION
"""
This is used to decorate fields which are for internal use, and are not
available to normal API users.
"""
directive @internal on FIELD_DEFINITION
enum AccessScope {
PROFILE @scopehelp(details: "profile information")
TRACKERS @scopehelp(details: "trackers")
@ -671,6 +677,14 @@ input SubmitTicketInput {
externalUrl: String
}
"For internal use only."
input SubmitEmailInput {
subject: String!
body: String
senderId: Int!
messageId: String!
}
"""
You may omit any fields to leave them unchanged. To remove the ticket body,
set it to null.
@ -803,6 +817,10 @@ type Mutation {
submitTicket(trackerId: Int!,
input: SubmitTicketInput!): Ticket @access(scope: TICKETS, kind: RW)
"Creates a new ticket from an incoming email. (For internal use only)"
submitEmail(trackerId: Int!,
input: SubmitEmailInput!): Ticket @internal
"Updates a ticket's subject or body"
updateTicket(trackerId: Int!, ticketId: Int!,
input: UpdateTicketInput!): Ticket @access(scope: TICKETS, kind: RW)

View File

@ -824,6 +824,116 @@ func (r *mutationResolver) SubmitTicket(ctx context.Context, trackerID int, inpu
return &ticket, nil
}
func (r *mutationResolver) SubmitEmail(ctx context.Context, trackerID int, input model.SubmitEmailInput) (*model.Ticket, error) {
validation := valid.New(ctx)
validation.Expect(len(input.Subject) <= 2048,
"Ticket subject must be fewer than to 2049 characters.").
WithField("subject")
if input.Body != nil {
validation.Expect(len(*input.Body) <= 16384,
"Ticket body must be less than 16 KiB in size").
WithField("body")
}
if !validation.Ok() {
return nil, nil
}
tracker, err := loaders.ForContext(ctx).TrackersByID.Load(trackerID)
if err != nil || tracker == nil {
return nil, err
}
owner, err := loaders.ForContext(ctx).UsersByID.Load(tracker.OwnerID)
if err != nil {
panic(err)
}
if !tracker.CanSubmit() {
return nil, fmt.Errorf("Access denied")
}
var ticket model.Ticket
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
row := tx.QueryRowContext(ctx, `
WITH tr AS (
UPDATE tracker
SET
next_ticket_id = next_ticket_id + 1,
updated = NOW() at time zone 'utc'
WHERE id = $1
RETURNING id, next_ticket_id, name
) INSERT INTO ticket (
created, updated,
tracker_id, scoped_id,
submitter_id, title, description
) VALUES (
NOW() at time zone 'utc',
NOW() at time zone 'utc',
(SELECT id FROM tr),
(SELECT next_ticket_id - 1 FROM tr),
$2, $3, $4
)
RETURNING
id, scoped_id, submitter_id, tracker_id, created, updated,
title, description, authenticity, status, resolution;`,
trackerID, input.SenderID, input.Subject, input.Body)
if err := row.Scan(&ticket.PKID, &ticket.ID, &ticket.SubmitterID,
&ticket.TrackerID, &ticket.Created, &ticket.Updated, &ticket.Subject,
&ticket.Body, &ticket.RawAuthenticity, &ticket.RawStatus,
&ticket.RawResolution); err != nil {
return err
}
ticket.OwnerName = owner.Username
ticket.TrackerName = tracker.Name
conf := config.ForContext(ctx)
origin := config.GetOrigin(conf, "todo.sr.ht", true)
builder := NewEventBuilder(ctx, tx, input.SenderID, model.EVENT_CREATED).
WithTicket(tracker, &ticket)
if ticket.Body != nil {
mentions := ScanMentions(ctx, tracker, &ticket, *ticket.Body)
builder.AddMentions(&mentions)
}
builder.InsertSubscriptions()
var eventID int
row = tx.QueryRowContext(ctx, `
INSERT INTO event (
created, event_type, participant_id, ticket_id
) VALUES (
NOW() at time zone 'utc',
$1, $2, $3
) RETURNING id;
`, model.EVENT_CREATED, input.SenderID, ticket.PKID)
if err := row.Scan(&eventID); err != nil {
panic(err)
}
builder.InsertNotifications(eventID, nil)
// TODO: In-Reply-To: {{input.MessageID}}
details := NewTicketDetails{
Body: ticket.Body,
Root: origin,
TicketURL: fmt.Sprintf("/%s/%s/%d",
owner.CanonicalName(), tracker.Name, ticket.ID),
}
subject := fmt.Sprintf("%s: %s", ticket.Ref(), ticket.Subject)
builder.SendEmails(subject, newTicketTemplate, &details)
return nil
}); err != nil {
return nil, err
}
webhooks.DeliverLegacyTicketCreate(ctx, tracker, &ticket)
webhooks.DeliverTicketEvent(ctx, model.WebhookEventTicketCreated, &ticket)
return &ticket, nil
}
func (r *mutationResolver) UpdateTicket(ctx context.Context, trackerID int, ticketID int, input map[string]interface{}) (*model.Ticket, error) {
if _, ok := input["import"]; ok {
panic(fmt.Errorf("not implemented")) // TODO

View File

@ -18,6 +18,7 @@ func main() {
appConfig := config.LoadConfig(":5103")
gqlConfig := api.Config{Resolvers: &graph.Resolver{}}
gqlConfig.Directives.Internal = server.Internal
gqlConfig.Directives.Access = func(ctx context.Context, obj interface{},
next graphql.Resolver, scope model.AccessScope,
kind model.AccessKind) (interface{}, error) {

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
from srht.config import cfg, get_origin
from srht.database import db, DbSession
from srht.graphql import exec_gql
db = DbSession(cfg("todo.sr.ht", "connection-string"))
import todosrht.types
db.init()
@ -11,7 +12,7 @@ from grp import getgrnam
from todosrht.access import get_tracker, get_ticket
from todosrht.types import TicketAccess, TicketResolution, Tracker, Ticket, User
from todosrht.types import Label, TicketLabel, TicketSubscription, Event, EventType, ParticipantType
from todosrht.tickets import add_comment, get_participant_for_email, submit_ticket
from todosrht.tickets import add_comment, get_participant_for_email
from todosrht.webhooks import UserWebhook, TrackerWebhook, TicketWebhook
from srht.validation import Validation
import asyncio
@ -136,31 +137,29 @@ class MailHandler:
print("Rejected, insufficient permissions")
return "550 You do not have permission to post on this tracker."
subject = mail["Subject"]
valid = Validation({
"title": subject,
"description": body,
})
valid = Validation({})
title = valid.require("title")
desc = valid.optional("description")
input = {
"subject": mail["Subject"],
"body": body,
"senderId": sender.id,
"messageId": mail["Message-ID"],
}
valid.expect(not title or 3 <= len(title) <= 2048,
"Title must be between 3 and 2048 characters",
field="title")
valid.expect(not desc or len(desc) < 16384,
"Description must be no more than 16384 characters",
field="description")
resp = exec_gql("todo.sr.ht", """
mutation SubmitEmail($trackerId: Int!, $input: SubmitEmailInput!) {
submitEmail(trackerId: $trackerId, input: $input) {
id
}
}
""", user=sender.user, valid=valid, trackerId=tracker.id, input=input)
if not valid.ok:
print("Rejecting email due to validation errors")
return "550 " + ", ".join([e.message for e in valid.errors])
ticket = submit_ticket(tracker, sender, title, desc,
from_email=True, from_email_id=mail["Message-ID"])
UserWebhook.deliver(UserWebhook.Events.ticket_create,
ticket.to_dict(),
UserWebhook.Subscription.user_id == sender.id)
ticket, _ = get_ticket(tracker, resp["submitEmail"]["id"], user=sender.user)
TrackerWebhook.deliver(TrackerWebhook.Events.ticket_create,
ticket.to_dict(),
TrackerWebhook.Subscription.tracker_id == tracker.id)