decred.org/dcrdex@v1.0.5/server/admin/api.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package admin 5 6 import ( 7 "encoding/hex" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "math" 13 "net/http" 14 "strconv" 15 "strings" 16 "time" 17 18 "decred.org/dcrdex/dex" 19 "decred.org/dcrdex/dex/msgjson" 20 "decred.org/dcrdex/dex/order" 21 "decred.org/dcrdex/server/account" 22 dexsrv "decred.org/dcrdex/server/dex" 23 "decred.org/dcrdex/server/market" 24 "github.com/go-chi/chi/v5" 25 ) 26 27 const ( 28 pongStr = "pong" 29 maxUInt16 = int(^uint16(0)) 30 ) 31 32 // writeJSON marshals the provided interface and writes the bytes to the 33 // ResponseWriter. The response code is assumed to be StatusOK. 34 func writeJSON(w http.ResponseWriter, thing any) { 35 writeJSONWithStatus(w, thing, http.StatusOK) 36 } 37 38 // writeJSON marshals the provided interface and writes the bytes to the 39 // ResponseWriter with the specified response code. 40 func writeJSONWithStatus(w http.ResponseWriter, thing any, code int) { 41 w.Header().Set("Content-Type", "application/json; charset=utf-8") 42 b, err := json.MarshalIndent(thing, "", " ") 43 if err != nil { 44 w.WriteHeader(http.StatusInternalServerError) 45 log.Errorf("JSON encode error: %v", err) 46 return 47 } 48 w.WriteHeader(code) 49 _, err = w.Write(append(b, byte('\n'))) 50 if err != nil { 51 log.Errorf("Write error: %v", err) 52 } 53 } 54 55 // apiPing is the handler for the '/ping' API request. 56 func apiPing(w http.ResponseWriter, _ *http.Request) { 57 writeJSON(w, pongStr) 58 } 59 60 // apiConfig is the handler for the '/config' API request. 61 func (s *Server) apiConfig(w http.ResponseWriter, _ *http.Request) { 62 writeJSON(w, s.core.ConfigMsg()) 63 } 64 65 // apiAsset is the handler for the '/asset/{"assetSymbol"}' API request. 66 func (s *Server) apiAsset(w http.ResponseWriter, r *http.Request) { 67 assetSymbol := strings.ToLower(chi.URLParam(r, assetSymbol)) 68 assetID, found := dex.BipSymbolID(assetSymbol) 69 if !found { 70 http.Error(w, fmt.Sprintf("unknown asset %q", assetSymbol), http.StatusBadRequest) 71 return 72 } 73 asset, err := s.core.Asset(assetID) 74 if err != nil { 75 http.Error(w, fmt.Sprintf("unsupported asset %q / %d", assetSymbol, assetID), http.StatusBadRequest) 76 return 77 } 78 79 var errs []string 80 backend := asset.Backend 81 var scaledFeeRate uint64 82 currentFeeRate, err := backend.FeeRate(r.Context()) 83 if err != nil { 84 errs = append(errs, fmt.Sprintf("unable to get current fee rate: %v", err)) 85 } else { 86 scaledFeeRate = s.core.ScaleFeeRate(assetID, currentFeeRate) 87 // Limit the scaled fee rate just as in (*Market).processReadyEpoch. 88 if scaledFeeRate > asset.MaxFeeRate { 89 scaledFeeRate = asset.MaxFeeRate 90 } 91 } 92 93 synced, err := backend.Synced() 94 if err != nil { 95 errs = append(errs, fmt.Sprintf("unable to get sync status: %v", err)) 96 } 97 98 res := &AssetInfo{ 99 Asset: asset.Asset, 100 CurrentFeeRate: currentFeeRate, 101 ScaledFeeRate: scaledFeeRate, 102 Synced: synced, 103 Errors: errs, 104 } 105 writeJSON(w, res) 106 } 107 108 // apiSetFeeScale is the handler for the 109 // '/asset/{"assetSymbol"}/setfeescale/{"scaleKey"}' API request. 110 func (s *Server) apiSetFeeScale(w http.ResponseWriter, r *http.Request) { 111 assetSymbol := strings.ToLower(chi.URLParam(r, assetSymbol)) 112 assetID, found := dex.BipSymbolID(assetSymbol) 113 if !found { 114 http.Error(w, fmt.Sprintf("unknown asset %q", assetSymbol), http.StatusBadRequest) 115 return 116 } 117 118 feeRateScaleStr := chi.URLParam(r, scaleKey) 119 feeRateScale, err := strconv.ParseFloat(feeRateScaleStr, 64) 120 if err != nil { 121 http.Error(w, fmt.Sprintf("invalid fee rate scale %q", feeRateScaleStr), http.StatusBadRequest) 122 return 123 } 124 125 _, err = s.core.Asset(assetID) // asset return may be used if other asset settings are modified 126 if err != nil { 127 http.Error(w, fmt.Sprintf("unsupported asset %q / %d", assetSymbol, assetID), http.StatusBadRequest) 128 return 129 } 130 131 log.Infof("Setting %s (%d) fee rate scale factor to %f", strings.ToUpper(assetSymbol), assetID, feeRateScale) 132 s.core.SetFeeRateScale(assetID, feeRateScale) 133 134 w.WriteHeader(http.StatusOK) 135 } 136 137 // apiMarkets is the handler for the '/markets' API request. 138 func (s *Server) apiMarkets(w http.ResponseWriter, r *http.Request) { 139 statuses := s.core.MarketStatuses() 140 mktStatuses := make(map[string]*MarketStatus) 141 for name, status := range statuses { 142 mktStatus := &MarketStatus{ 143 // Name is empty since the key is the name. 144 Running: status.Running, 145 EpochDuration: status.EpochDuration, 146 ActiveEpoch: status.ActiveEpoch, 147 StartEpoch: status.StartEpoch, 148 SuspendEpoch: status.SuspendEpoch, 149 } 150 if status.SuspendEpoch != 0 { 151 persist := status.PersistBook 152 mktStatus.PersistBook = &persist 153 } 154 mktStatuses[name] = mktStatus 155 } 156 157 writeJSON(w, mktStatuses) 158 } 159 160 // apiMarketInfo is the handler for the '/market/{marketName}' API request. 161 func (s *Server) apiMarketInfo(w http.ResponseWriter, r *http.Request) { 162 mkt := strings.ToLower(chi.URLParam(r, marketNameKey)) 163 status := s.core.MarketStatus(mkt) 164 if status == nil { 165 http.Error(w, fmt.Sprintf("unknown market %q", mkt), http.StatusBadRequest) 166 return 167 } 168 169 var persist *bool 170 if status.SuspendEpoch != 0 { 171 persistLocal := status.PersistBook 172 persist = &persistLocal 173 } 174 mktStatus := &MarketStatus{ 175 Name: mkt, 176 Running: status.Running, 177 EpochDuration: status.EpochDuration, 178 ActiveEpoch: status.ActiveEpoch, 179 StartEpoch: status.StartEpoch, 180 SuspendEpoch: status.SuspendEpoch, 181 PersistBook: persist, 182 } 183 if status.SuspendEpoch != 0 { 184 persist := status.PersistBook 185 mktStatus.PersistBook = &persist 186 } 187 writeJSON(w, mktStatus) 188 } 189 190 // apiMarketOrderBook is the handler for the '/market/{marketName}/orderbook' 191 // API request. 192 func (s *Server) apiMarketOrderBook(w http.ResponseWriter, r *http.Request) { 193 mkt := strings.ToLower(chi.URLParam(r, marketNameKey)) 194 status := s.core.MarketStatus(mkt) 195 if status == nil { 196 http.Error(w, fmt.Sprintf("unknown market %q", mkt), http.StatusBadRequest) 197 return 198 } 199 orders, err := s.core.BookOrders(status.Base, status.Quote) 200 if err != nil { 201 http.Error(w, fmt.Sprintf("failed to obtain order book: %v", err), http.StatusInternalServerError) 202 return 203 } 204 msgBook := make([]*msgjson.BookOrderNote, 0, len(orders)) 205 for _, o := range orders { 206 msgOrder, err := market.OrderToMsgOrder(o, mkt) 207 if err != nil { 208 log.Errorf("unable to encode order: %w", err) 209 continue 210 } 211 msgBook = append(msgBook, msgOrder) 212 } 213 // This is a msgjson.OrderBook without the seq field. 214 res := &struct { 215 MarketID string `json:"marketid"` 216 Epoch uint64 `json:"epoch"` 217 Orders []*msgjson.BookOrderNote `json:"orders"` 218 }{ 219 MarketID: mkt, 220 Epoch: uint64(status.ActiveEpoch), 221 Orders: msgBook, 222 } 223 writeJSON(w, res) 224 } 225 226 // handler for route '/market/{marketName}/epochorders' API request. 227 func (s *Server) apiMarketEpochOrders(w http.ResponseWriter, r *http.Request) { 228 mkt := strings.ToLower(chi.URLParam(r, marketNameKey)) 229 status := s.core.MarketStatus(mkt) 230 if status == nil { 231 http.Error(w, fmt.Sprintf("unknown market %q", mkt), http.StatusBadRequest) 232 return 233 } 234 orders, err := s.core.EpochOrders(status.Base, status.Quote) 235 if err != nil { 236 http.Error(w, fmt.Sprintf("failed to obtain epoch orders: %v", err), http.StatusInternalServerError) 237 return 238 } 239 msgBook := make([]*msgjson.BookOrderNote, 0, len(orders)) 240 for _, o := range orders { 241 msgOrder, err := market.OrderToMsgOrder(o, mkt) 242 if err != nil { 243 log.Errorf("unable to encode order: %w", err) 244 continue 245 } 246 msgBook = append(msgBook, msgOrder) 247 } 248 // This is a msgjson.OrderBook without the seq field. 249 res := &struct { 250 MarketID string `json:"marketid"` 251 Epoch uint64 `json:"epoch"` 252 Orders []*msgjson.BookOrderNote `json:"orders"` 253 }{ 254 MarketID: mkt, 255 Epoch: uint64(status.ActiveEpoch), 256 Orders: msgBook, 257 } 258 writeJSON(w, res) 259 } 260 261 // handler for route '/market/{marketName}/matches?includeinactive=BOOL&n=INT' API 262 // request. The n value is only used when includeinactive is true. 263 func (s *Server) apiMarketMatches(w http.ResponseWriter, r *http.Request) { 264 var includeInactive bool 265 if includeInactiveStr := r.URL.Query().Get(includeInactiveKey); includeInactiveStr != "" { 266 var err error 267 includeInactive, err = strconv.ParseBool(includeInactiveStr) 268 if err != nil { 269 http.Error(w, fmt.Sprintf("invalid include inactive boolean %q: %v", includeInactiveStr, err), http.StatusBadRequest) 270 return 271 } 272 } 273 var N int64 // <=0 is all 274 if nStr := r.URL.Query().Get(nKey); nStr != "" { 275 var err error 276 N, err = strconv.ParseInt(nStr, 10, 64) 277 if err != nil { 278 http.Error(w, fmt.Sprintf("invalid n int %q: %v", nStr, err), http.StatusBadRequest) 279 return 280 } 281 } 282 283 mkt := strings.ToLower(chi.URLParam(r, marketNameKey)) 284 status := s.core.MarketStatus(mkt) 285 if status == nil { 286 http.Error(w, fmt.Sprintf("unknown market %q", mkt), http.StatusBadRequest) 287 return 288 } 289 290 w.Header().Set("Content-Type", "application/json; charset=utf-8") 291 292 // The response is not an array, but a series of objects. TIP: Use "jq -s" 293 // to generate an array of these objects. 294 enc := json.NewEncoder(w) 295 enc.SetIndent("", " ") 296 Nout, err := s.core.MarketMatchesStreaming(status.Base, status.Quote, includeInactive, N, 297 func(match *dexsrv.MatchData) error { 298 md := &MatchData{ 299 TakerSell: match.TakerSell, 300 ID: match.ID.String(), 301 Maker: match.Maker.String(), 302 MakerAcct: match.MakerAcct.String(), 303 MakerSwap: match.MakerSwap, 304 MakerRedeem: match.MakerRedeem, 305 MakerAddr: match.MakerAddr, 306 Taker: match.Taker.String(), 307 TakerAcct: match.TakerAcct.String(), 308 TakerSwap: match.TakerSwap, 309 TakerRedeem: match.TakerRedeem, 310 TakerAddr: match.TakerAddr, 311 EpochIdx: match.Epoch.Idx, 312 EpochDur: match.Epoch.Dur, 313 Quantity: match.Quantity, 314 Rate: match.Rate, 315 BaseRate: match.BaseRate, 316 QuoteRate: match.QuoteRate, 317 Active: match.Active, 318 Status: match.Status.String(), 319 } 320 return enc.Encode(md) 321 }) 322 if err != nil { 323 log.Warnf("Failed to write matches response: %v", err) 324 if Nout == 0 { 325 http.Error(w, fmt.Sprintf("failed to retrieve matches: %v", err), http.StatusInternalServerError) 326 } // otherwise too late for an http error code 327 } 328 } 329 330 // handler for route '/market/{marketName}/resume?t=UNIXMS' 331 func (s *Server) apiResume(w http.ResponseWriter, r *http.Request) { 332 // Ensure the market exists and is not running. 333 mkt := strings.ToLower(chi.URLParam(r, marketNameKey)) 334 found, running := s.core.MarketRunning(mkt) 335 if !found { 336 http.Error(w, fmt.Sprintf("unknown market %q", mkt), http.StatusBadRequest) 337 return 338 } 339 if running { 340 http.Error(w, fmt.Sprintf("market %q running", mkt), http.StatusBadRequest) 341 return 342 } 343 344 // Validate the resume time provided in the "t" query. If not specified, 345 // the zero time.Time is used to indicate ASAP. 346 var resTime time.Time 347 if tResumeStr := r.URL.Query().Get("t"); tResumeStr != "" { 348 resTimeMs, err := strconv.ParseInt(tResumeStr, 10, 64) 349 if err != nil { 350 http.Error(w, fmt.Sprintf("invalid resume time %q: %v", tResumeStr, err), http.StatusBadRequest) 351 return 352 } 353 354 resTime = time.UnixMilli(resTimeMs) 355 if time.Until(resTime) < 0 { 356 http.Error(w, fmt.Sprintf("specified market resume time is in the past: %v", resTime), 357 http.StatusBadRequest) 358 return 359 } 360 } 361 362 resEpoch, resTime, err := s.core.ResumeMarket(mkt, resTime) 363 if resEpoch == 0 || err != nil { 364 // Should not happen. 365 msg := fmt.Sprintf("Failed to resume market: %v", err) 366 log.Errorf(msg) 367 http.Error(w, msg, http.StatusInternalServerError) 368 return 369 } 370 371 writeJSON(w, &ResumeResult{ 372 Market: mkt, 373 StartEpoch: resEpoch, 374 StartTime: APITime{resTime}, 375 }) 376 } 377 378 // handler for route '/market/{marketName}/suspend?t=UNIXMS&persist=BOOL' 379 func (s *Server) apiSuspend(w http.ResponseWriter, r *http.Request) { 380 // Ensure the market exists and is running. 381 mkt := strings.ToLower(chi.URLParam(r, marketNameKey)) 382 found, running := s.core.MarketRunning(mkt) 383 if !found { 384 http.Error(w, fmt.Sprintf("unknown market %q", mkt), http.StatusBadRequest) 385 return 386 } 387 if !running { 388 http.Error(w, fmt.Sprintf("market %q not running", mkt), http.StatusBadRequest) 389 return 390 } 391 392 // Validate the suspend time provided in the "t" query. If not specified, 393 // the zero time.Time is used to indicate ASAP. 394 var suspTime time.Time 395 if tSuspendStr := r.URL.Query().Get("t"); tSuspendStr != "" { 396 suspTimeMs, err := strconv.ParseInt(tSuspendStr, 10, 64) 397 if err != nil { 398 http.Error(w, fmt.Sprintf("invalid suspend time %q: %v", tSuspendStr, err), http.StatusBadRequest) 399 return 400 } 401 402 suspTime = time.UnixMilli(suspTimeMs) 403 if time.Until(suspTime) < 0 { 404 http.Error(w, fmt.Sprintf("specified market suspend time is in the past: %v", suspTime), 405 http.StatusBadRequest) 406 return 407 } 408 } 409 410 // Validate the persist book flag provided in the "persist" query. If not 411 // specified, persist the books, do not purge. 412 persistBook := true 413 if persistBookStr := r.URL.Query().Get("persist"); persistBookStr != "" { 414 var err error 415 persistBook, err = strconv.ParseBool(persistBookStr) 416 if err != nil { 417 http.Error(w, fmt.Sprintf("invalid persist book boolean %q: %v", persistBookStr, err), http.StatusBadRequest) 418 return 419 } 420 } 421 422 suspEpoch, err := s.core.SuspendMarket(mkt, suspTime, persistBook) 423 if suspEpoch == nil || err != nil { 424 // Should not happen. 425 msg := fmt.Sprintf("Failed to suspend market: %v", err) 426 log.Errorf(msg) 427 http.Error(w, msg, http.StatusInternalServerError) 428 return 429 } 430 431 writeJSON(w, &SuspendResult{ 432 Market: mkt, 433 FinalEpoch: suspEpoch.Idx, 434 SuspendTime: APITime{suspEpoch.End}, 435 }) 436 } 437 438 // apiEnableDataAPI is the handler for the `/enabledataapi/{yes}` API request, 439 // used to enable or disable the HTTP data API. 440 func (s *Server) apiEnableDataAPI(w http.ResponseWriter, r *http.Request) { 441 yes, err := strconv.ParseBool(chi.URLParam(r, yesKey)) 442 if err != nil { 443 http.Error(w, "unable to parse selection: "+err.Error(), http.StatusBadRequest) 444 return 445 } 446 s.core.EnableDataAPI(yes) 447 msg := "Data API disabled" 448 if yes { 449 msg = "Data API enabled" 450 } 451 writeJSON(w, msg) 452 } 453 454 // apiAccountInfo is the handler for the '/account/{account id}' API request. 455 func (s *Server) apiAccountInfo(w http.ResponseWriter, r *http.Request) { 456 acctIDStr := chi.URLParam(r, accountIDKey) 457 acctIDSlice, err := hex.DecodeString(acctIDStr) 458 if err != nil { 459 http.Error(w, fmt.Sprintf("could not decode account id: %v", err), http.StatusBadRequest) 460 return 461 } 462 if len(acctIDSlice) != account.HashSize { 463 http.Error(w, "account id has incorrect length", http.StatusBadRequest) 464 return 465 } 466 var acctID account.AccountID 467 copy(acctID[:], acctIDSlice) 468 acctInfo, err := s.core.AccountInfo(acctID) 469 if err != nil { 470 http.Error(w, fmt.Sprintf("failed to retrieve account: %v", err), http.StatusInternalServerError) 471 return 472 } 473 writeJSON(w, acctInfo) 474 } 475 476 func (s *Server) prepayBonds(w http.ResponseWriter, r *http.Request) { 477 var n int = 1 478 if nStr := r.URL.Query().Get(nKey); nStr != "" { 479 n64, err := strconv.ParseUint(nStr, 10, 16) 480 if err != nil { 481 http.Error(w, fmt.Sprintf("error parsing n: %v", err), http.StatusBadRequest) 482 return 483 } 484 n = int(n64) 485 } 486 if n < 0 || n > 100 { 487 http.Error(w, "requested too many prepaid bonds. max 100", http.StatusBadRequest) 488 return 489 } 490 daysStr := r.URL.Query().Get(daysKey) 491 if daysStr == "" { 492 http.Error(w, "no days duration specified", http.StatusBadRequest) 493 return 494 } 495 days, err := strconv.ParseUint(daysStr, 10, 64) 496 if err != nil { 497 http.Error(w, fmt.Sprintf("error parsing days: %v", err), http.StatusBadRequest) 498 return 499 } 500 if days == 0 { 501 http.Error(w, "days parsed to zero", http.StatusBadRequest) 502 return 503 } 504 dur := time.Duration(days) * time.Hour * 24 505 var strength uint32 = 1 506 if strengthStr := r.URL.Query().Get(strengthKey); strengthStr != "" { 507 n64, err := strconv.ParseUint(strengthStr, 10, 32) 508 if err != nil { 509 http.Error(w, fmt.Sprintf("error parsing strength: %v", err), http.StatusBadRequest) 510 return 511 } 512 strength = uint32(n64) 513 } 514 coinIDs, err := s.core.CreatePrepaidBonds(n, strength, int64(math.Round(dur.Seconds()))) 515 if err != nil { 516 http.Error(w, fmt.Sprintf("error creating bonds: %v", err), http.StatusInternalServerError) 517 return 518 } 519 res := make([]dex.Bytes, len(coinIDs)) 520 for i := range coinIDs { 521 res[i] = coinIDs[i] 522 } 523 writeJSON(w, res) 524 } 525 526 // decodeAcctID checks a string as being both hex and the right length and 527 // returns its bytes encoded as an account.AccountID. 528 func decodeAcctID(acctIDStr string) (account.AccountID, error) { 529 var acctID account.AccountID 530 if len(acctIDStr) != account.HashSize*2 { 531 return acctID, errors.New("account id has incorrect length") 532 } 533 if _, err := hex.Decode(acctID[:], []byte(acctIDStr)); err != nil { 534 return acctID, fmt.Errorf("could not decode account id: %w", err) 535 } 536 return acctID, nil 537 } 538 539 // apiForgiveMatchFail is the handler for the '/account/{accountID}/forgive_match/{matchID}' API request. 540 func (s *Server) apiForgiveMatchFail(w http.ResponseWriter, r *http.Request) { 541 acctIDStr := chi.URLParam(r, accountIDKey) 542 acctID, err := decodeAcctID(acctIDStr) 543 if err != nil { 544 http.Error(w, err.Error(), http.StatusBadRequest) 545 return 546 } 547 matchIDStr := chi.URLParam(r, matchIDKey) 548 matchID, err := order.DecodeMatchID(matchIDStr) 549 if err != nil { 550 http.Error(w, err.Error(), http.StatusBadRequest) 551 return 552 } 553 forgiven, unbanned, err := s.core.ForgiveMatchFail(acctID, matchID) 554 if err != nil { 555 http.Error(w, fmt.Sprintf("failed to forgive failed match %v for account %v: %v", matchID, acctID, err), http.StatusInternalServerError) 556 return 557 } 558 res := ForgiveResult{ 559 AccountID: acctIDStr, 560 Forgiven: forgiven, 561 Unbanned: unbanned, 562 ForgiveTime: APITime{time.Now()}, 563 } 564 writeJSON(w, res) 565 } 566 567 func (s *Server) apiMatchOutcomes(w http.ResponseWriter, r *http.Request) { 568 acctIDStr := chi.URLParam(r, accountIDKey) 569 acctID, err := decodeAcctID(acctIDStr) 570 if err != nil { 571 http.Error(w, err.Error(), http.StatusBadRequest) 572 return 573 } 574 var n int = 100 575 if nStr := r.URL.Query().Get("n"); nStr != "" { 576 n, err = strconv.Atoi(nStr) 577 if err != nil { 578 http.Error(w, err.Error(), http.StatusBadRequest) 579 return 580 } 581 } 582 outcomes, err := s.core.AccountMatchOutcomesN(acctID, n) 583 if err != nil { 584 http.Error(w, err.Error(), http.StatusInternalServerError) 585 return 586 } 587 writeJSON(w, outcomes) 588 } 589 590 func (s *Server) apiMatchFails(w http.ResponseWriter, r *http.Request) { 591 acctIDStr := chi.URLParam(r, accountIDKey) 592 acctID, err := decodeAcctID(acctIDStr) 593 if err != nil { 594 http.Error(w, err.Error(), http.StatusBadRequest) 595 return 596 } 597 var n int = 100 598 if nStr := r.URL.Query().Get("n"); nStr != "" { 599 n, err = strconv.Atoi(nStr) 600 if err != nil { 601 http.Error(w, err.Error(), http.StatusBadRequest) 602 return 603 } 604 } 605 fails, err := s.core.UserMatchFails(acctID, n) 606 if err != nil { 607 http.Error(w, err.Error(), http.StatusInternalServerError) 608 return 609 } 610 writeJSON(w, fails) 611 } 612 613 func toNote(r *http.Request) (*msgjson.Message, int, error) { 614 body, err := io.ReadAll(r.Body) 615 r.Body.Close() 616 if err != nil { 617 return nil, http.StatusInternalServerError, fmt.Errorf("unable to read request body: %w", err) 618 } 619 if len(body) == 0 { 620 return nil, http.StatusBadRequest, errors.New("no message to broadcast") 621 } 622 // Remove trailing newline if present. A newline is added by the curl 623 // command when sending from file. 624 if body[len(body)-1] == '\n' { 625 body = body[:len(body)-1] 626 } 627 if len(body) > maxUInt16 { 628 return nil, http.StatusBadRequest, fmt.Errorf("cannot send messages larger than %d bytes", maxUInt16) 629 } 630 msg, err := msgjson.NewNotification(msgjson.NotifyRoute, string(body)) 631 if err != nil { 632 return nil, http.StatusInternalServerError, fmt.Errorf("unable to create notification: %w", err) 633 } 634 return msg, 0, nil 635 } 636 637 // apiNotify is the handler for the '/account/{accountID}/notify' API request. 638 func (s *Server) apiNotify(w http.ResponseWriter, r *http.Request) { 639 acctIDStr := chi.URLParam(r, accountIDKey) 640 acctID, err := decodeAcctID(acctIDStr) 641 if err != nil { 642 http.Error(w, err.Error(), http.StatusBadRequest) 643 return 644 } 645 msg, errCode, err := toNote(r) 646 if err != nil { 647 http.Error(w, err.Error(), errCode) 648 return 649 } 650 s.core.Notify(acctID, msg) 651 w.WriteHeader(http.StatusOK) 652 } 653 654 // apiNotifyAll is the handler for the '/notifyall' API request. 655 func (s *Server) apiNotifyAll(w http.ResponseWriter, r *http.Request) { 656 msg, errCode, err := toNote(r) 657 if err != nil { 658 http.Error(w, err.Error(), errCode) 659 return 660 } 661 s.core.NotifyAll(msg) 662 w.WriteHeader(http.StatusOK) 663 }