package pretix import ( "encoding/json" "fmt" "math/rand" "net/http" "strconv" "strings" "time" "maunium.net/go/mautrix/event" ) const ( eventOrderPlaced = "pretix.event.order.placed" urlIndividualOrder = "%s/api/v1/organizers/%s/events/%s/orders/%s/" urlItem = "%s/api/v1/organizers/%s/events/%s/items/%d/" urlItemCategory = "%s/api/v1/organizers/%s/events/%s/categories/%d/" ) var adverbs = []string{" Außerdem ", " Zusätzlich ", " Weiterhin ", " Des Weiteren "} type categoryBin struct { Cents uint64 Euros uint64 NumFree uint NumPaid uint } func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Content-Type") != "application/json" { writeStatus(w, http.StatusBadRequest) return } data := struct{ Action string Code string Event string Organizer string }{} decoder := json.NewDecoder(r.Body) err := decoder.Decode(&data) if err != nil { writeStatus(w, http.StatusBadRequest) return } if data.Action != eventOrderPlaced { writeStatus(w, http.StatusBadRequest) return } resp, err := s.doGetRequest(s.buildURL(urlIndividualOrder, data.Organizer, data.Event, data.Code)) if err != nil { writeStatus(w, http.StatusInternalServerError) return } defer resp.Body.Close() if resp.StatusCode != 200 { writeStatus(w, http.StatusInternalServerError) return } if resp.Header.Get("Content-Type") != "application/json" { writeStatus(w, http.StatusInternalServerError) return } decoder = json.NewDecoder(resp.Body) order := struct{ Code string Datetime time.Time Positions []struct{ Item uint Price string } }{} err = decoder.Decode(&order) if err != nil { writeStatus(w, http.StatusInternalServerError) return } result, err := s.db.Exec("INSERT INTO orders VALUES(?);", order.Code) if err != nil { writeStatus(w, http.StatusInternalServerError) return } rowsAffected, err := result.RowsAffected() if err != nil { writeStatus(w, http.StatusInternalServerError) return } if rowsAffected > 0 && order.Datetime.Add(72 * time.Hour).After(time.Now()) { // pretix retries failed webhooks for up to three days categories := map[string]categoryBin{} for _, position := range order.Positions { euros, cents, err := parsePrice(position.Price) if err != nil { continue } name := s.fetchCategory(data.Organizer, data.Event, position.Item) category := categories[name] if euros > 0 || cents > 0 { category.Cents += cents if carry := category.Cents / 100; carry > 0 { category.Cents -= carry * 100 category.Euros += carry } category.Euros += euros category.NumPaid++ } else { category.NumFree++ } categories[name] = category } var body strings.Builder body.WriteString("\U0001f389") rng := rand.New(rand.NewSource(time.Now().Unix())) adverb := " Es " for category, bin := range categories { var verb, free, conjunction, paid, noun, preposition, quote, total string if bin.NumFree+bin.NumPaid == 1 { verb = "wurde " } else { verb = "wurden " } if bin.NumFree == 1 { free = "ein kostenloses " } else if bin.NumFree > 1 { free = fmt.Sprint(bin.NumFree, " kostenlose ") } if bin.NumPaid == 1 { paid = "ein bezahltes " } else if bin.NumPaid > 1 { paid = fmt.Sprint(bin.NumPaid, " bezahlte ") } if bin.NumFree > 0 && bin.NumPaid > 0 { conjunction = "und " } if bin.NumPaid == 1 || bin.NumPaid == 0 && bin.NumFree == 1 { // match number of adjective immediately before noun noun = "Produkt " } else { noun = "Produkte " } if category != "" { preposition = `aus der Kategorie "` quote = `" ` } if bin.Euros > 0 || bin.Cents > 0 { var adverb, fraction string if bin.NumPaid > 1 { adverb = "insgesamt " } if bin.Cents > 0 { fraction = fmt.Sprintf(",%02d", bin.Cents) } total = fmt.Sprint("für ", adverb, bin.Euros, fraction, " Euro ") } body.WriteString(fmt.Sprint(adverb, verb, free, conjunction, paid, noun, preposition, category, quote, total, "bestellt.")) adverb = adverbs[rng.Intn(len(adverbs))] } message := event.MessageEventContent{ MsgType: event.MsgText, Body: body.String(), } success := s.matrix.Broadcast(&message) if !success { writeStatus(w, http.StatusInternalServerError) } } } func (s Server) fetchCategory(organizer, event string, item uint) string { resp, err := s.doGetRequest(s.buildURL(urlItem, organizer, event, item)) if err != nil { return "" } defer resp.Body.Close() if resp.StatusCode != 200 || resp.Header.Get("Content-Type") != "application/json" { return "" } decoder := json.NewDecoder(resp.Body) itemData := struct{ Category uint }{} err = decoder.Decode(&itemData) if err != nil || itemData.Category == 0 { return "" } resp, err = s.doGetRequest(s.buildURL(urlItemCategory, organizer, event, itemData.Category)) if err != nil { return "" } defer resp.Body.Close() if resp.StatusCode != 200 || resp.Header.Get("Content-Type") != "application/json" { return "" } decoder = json.NewDecoder(resp.Body) itemCategory := struct{ InternalName string `json:"internal_name"` }{} err = decoder.Decode(&itemCategory) if err != nil { return "" } return itemCategory.InternalName } func parsePrice(price string) (euros uint64, cents uint64, err error) { parts := strings.Split(price, ".") euros, err = strconv.ParseUint(parts[0], 10, 64) if err != nil { return 0, 0, err } if len(parts) > 1 { for len(parts[1]) < 2 { parts[1] += "0" } parts[1] = parts[1][:2] parts[1] = strings.TrimLeft(parts[1], "0") if parts[1] == "" { parts[1] = "0" } cents, err = strconv.ParseUint(parts[1], 10, 64) if err != nil { return 0, 0, err } } return euros, cents, nil }