todo.sr.ht/todosrht/tracker_import.py

208 lines
8.7 KiB
Python

import json
from collections import OrderedDict
from datetime import datetime, timezone
from srht.crypto import verify_payload
from srht.config import get_origin
from srht.database import db
from todosrht.tickets import submit_ticket
from todosrht.tickets import get_participant_for_email
from todosrht.tickets import get_participant_for_external
from todosrht.tickets import get_participant_for_user
from todosrht.types import Event, EventType, Tracker, Ticket, TicketComment
from todosrht.types import ParticipantType, User
from todosrht.types import Label, TicketLabel
from todosrht.types import TicketStatus, TicketResolution
from todosrht.types import TicketAuthenticity
from todosrht.webhooks import worker
our_upstream = get_origin("todo.sr.ht", external=True)
def _parse_date(date):
date = datetime.fromisoformat(date)
date = date.astimezone(timezone.utc).replace(tzinfo=None)
return date
def _import_participant(pdata, upstream):
if pdata["type"] == "user":
if upstream == our_upstream:
user = User.query.filter(User.username == pdata["name"]).first()
else:
user = None
if user:
submitter = get_participant_for_user(user)
else:
submitter = get_participant_for_external(
pdata["canonical_name"],
upstream + "/" + pdata["canonical_name"])
elif pdata["type"] == "email":
submitter = get_participant_for_email(pdata["address"], pdata["name"])
elif pdata["type"] == "external":
submitter = get_participant_for_external(
pdata["external_id"], pdata["external_url"])
return submitter
def _import_comment(ticket, event, edata):
cdata = edata["comment"]
comment = TicketComment()
submitter = _import_participant(cdata["submitter"], edata["upstream"])
comment.submitter_id = submitter.id
comment.ticket_id = ticket.id
comment.text = cdata["text"]
comment.authenticity = TicketAuthenticity.unauthenticated
comment.created = _parse_date(cdata["created"])
comment.updated = comment.created
comment._no_autoupdate = True
signature, nonce = edata.get("X-Payload-Signature"), edata.get("X-Payload-Nonce")
if (signature and nonce
and edata["upstream"] == our_upstream
and submitter.participant_type == ParticipantType.user):
# TODO: Validate signatures from trusted third-parties
sigdata = OrderedDict({
"comment": comment.text,
"id": edata["id"], # not important to verify this
"ticket": edata["ticket"]["ref"], # not important to verify this
"user": submitter.user.canonical_name,
"upstream": edata["upstream"],
})
sigdata = json.dumps(sigdata)
if verify_payload(sigdata, signature, nonce):
comment.authenticity = TicketAuthenticity.authentic
else:
comment.authenticity = TicketAuthenticity.tampered
ticket.comment_count += 1
db.session.add(comment)
db.session.flush()
event.comment_id = comment.id
def _tracker_import(dump, tracker):
ldict = dict()
for ldata in dump["labels"]:
label = Label()
label.tracker_id = tracker.id
label.name = ldata["name"]
label.color = ldata["colors"]["background"]
label.text_color = ldata["colors"]["text"]
db.session.add(label)
db.session.flush()
ldict[label.name] = label.id
tickets = sorted(dump["tickets"], key=lambda t: t["id"])
for tdata in tickets:
for field in [
"id", "title", "created", "description", "status", "resolution",
"labels", "assignees", "upstream", "events", "submitter",
]:
if not field in tdata:
print("Invalid ticket data")
continue
ticket = Ticket.query.filter(
Ticket.tracker_id == tracker.id,
Ticket.scoped_id == tdata["id"]).one_or_none()
if ticket:
print(f"Ticket {tdata['id']} already imported - skipping")
continue
submitter = _import_participant(tdata["submitter"], tdata["upstream"])
ticket = submit_ticket(tracker, submitter,
tdata["title"], tdata["description"], importing=True)
try:
created = _parse_date(tdata["created"])
except ValueError:
created = datetime.utcnow()
ticket._no_autoupdate = True
ticket.created = created
ticket.updated = created
ticket.status = TicketStatus[tdata["status"]]
ticket.resolution = TicketResolution[tdata["resolution"]]
ticket.authenticity = TicketAuthenticity.unauthenticated
for label in tdata["labels"]:
tl = TicketLabel()
tl.ticket_id = ticket.id
tl.label_id = ldict.get(label)
tl.user_id = tracker.owner_id
db.session.add(tl)
# TODO: assignees
signature, nonce = tdata.get("X-Payload-Signature"), tdata.get("X-Payload-Nonce")
if (signature and nonce
and tdata["upstream"] == our_upstream
and submitter.participant_type == ParticipantType.user):
# TODO: Validate signatures from trusted third-parties
sigdata = OrderedDict({
"description": ticket.description,
"ref": tdata["ref"], # not important to verify this
"submitter": ticket.submitter.user.canonical_name,
"title": ticket.title,
"upstream": tdata["upstream"],
})
sigdata = json.dumps(sigdata)
if verify_payload(sigdata, signature, nonce):
ticket.authenticity = TicketAuthenticity.authentic
else:
ticket.authenticity = TicketAuthenticity.tampered
for edata in tdata.get("events", []):
for field in [
"created", "event_type", "old_status", "new_status",
"old_resolution", "new_resolution", "user", "ticket",
"comment", "label", "by_user", "from_ticket", "upstream",
]:
if not field in edata:
print("Invalid ticket event")
return
event = Event()
for etype in edata["event_type"]:
if event.event_type == None:
event.event_type = EventType[etype]
else:
event.event_type |= EventType[etype]
if event.event_type == None:
print("Invalid ticket event")
continue
if EventType.comment in event.event_type:
_import_comment(ticket, event, edata)
if EventType.status_change in event.event_type:
if edata["old_status"]:
event.old_status = TicketStatus[edata["old_status"]]
if edata["new_status"]:
event.new_status = TicketStatus[edata["new_status"]]
if EventType.label_added in event.event_type:
event.label_id = ldict.get(edata["label"])
if not event.label_id:
continue
if EventType.label_removed in event.event_type:
event.label_id = ldict.get(edata["label"])
if not event.label_id:
continue
if EventType.assigned_user in event.event_type:
by_participant = _import_participant(
edata["by_user"], edata["upstream"])
event.by_participant_id = by_participant.id
if EventType.unassigned_user in event.event_type:
by_participant = _import_participant(
edata["by_user"], edata["upstream"])
event.by_participant_id = by_participant.id
if EventType.user_mentioned in event.event_type:
continue # Magic event type, do not import
if EventType.ticket_mentioned in event.event_type:
continue # TODO: Could reference tickets imported in later iters
event.created = _parse_date(edata["created"])
event.updated = event.created
event._no_autoupdate = True
event.ticket_id = ticket.id
participant = _import_participant(edata["user"], edata["upstream"])
event.participant_id = participant.id
db.session.add(event)
print(f"Imported {ticket.ref()}")
@worker.task
def tracker_import(dump, tracker_id):
tracker = Tracker.query.get(tracker_id)
try:
_tracker_import(dump, tracker)
except:
# TODO: Tell user that the import failed?
db.session.rollback()
tracker = Tracker.query.get(tracker_id)
raise
finally:
tracker.import_in_progress = False
db.session.commit()