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:
parent
ebb55fc95a
commit
dee9e75762
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue