feat(mail): draft, attach, and draft-invoice CLI
Save OO Mail drafts via body field, attach Files documents, and one-shot invoice PDF regenerate + draft for client billing emails.
This commit is contained in:
@@ -631,7 +631,7 @@ oo tasks files detach 208 12345
|
|||||||
| `companies` | `list`, `create`, `delete`, `dedupe`, `dedupe-persons` |
|
| `companies` | `list`, `create`, `delete`, `dedupe`, `dedupe-persons` |
|
||||||
| `opportunities` | `list`, `get`, `create`, `delete`, `stages`, `member-add`, `dedupe`, `dedupe-members`, `fix-titles` |
|
| `opportunities` | `list`, `get`, `create`, `delete`, `stages`, `member-add`, `dedupe`, `dedupe-members`, `fix-titles` |
|
||||||
| `crm` | `cleanup` |
|
| `crm` | `cleanup` |
|
||||||
| `mails` | `accounts`, `folders`, `list`, `get`, `delete` |
|
| `mails` | `accounts`, `folders`, `list`, `get`, `draft`, `attach`, `draft-invoice`, `delete` |
|
||||||
| `cases` | `list`, `create`, `delete`, `member-add` |
|
| `cases` | `list`, `create`, `delete`, `member-add` |
|
||||||
| `crm-tasks` | `list`, `create`, `delete`, `categories` |
|
| `crm-tasks` | `list`, `create`, `delete`, `categories` |
|
||||||
| `applications` | `sync` |
|
| `applications` | `sync` |
|
||||||
@@ -821,6 +821,16 @@ oo mails get 5664 -o json | jq '{subject, from, to, date}'
|
|||||||
# Remove one or more messages (server moves to trash or deletes per Mail rules)
|
# Remove one or more messages (server moves to trash or deletes per Mail rules)
|
||||||
oo mails delete 5664
|
oo mails delete 5664
|
||||||
oo mails delete 5664 5663 5661
|
oo mails delete 5664 5663 5661
|
||||||
|
|
||||||
|
# Create / update a draft (HTML body field is API "body"; plain text is wrapped)
|
||||||
|
oo mails draft --to client@example.com --subject "Rechnung INV-2026-01" \
|
||||||
|
--body "Guten Tag,\n\nanbei die Rechnung.\n\nMit freundlichen Grüßen"
|
||||||
|
|
||||||
|
# Attach an OnlyOffice Files document (e.g. invoice PDF file id) to a draft
|
||||||
|
oo mails attach 7301 --file-id 12345
|
||||||
|
|
||||||
|
# Regenerate invoice PDF + draft + attach (does not send)
|
||||||
|
oo mails draft-invoice --invoice 16 --to info@example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
**Table output** splits the `from` header into `fromName` and `fromAddress`
|
**Table output** splits the `from` header into `fromName` and `fromAddress`
|
||||||
|
|||||||
+175
-1
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
var mailsCmd = &cobra.Command{
|
var mailsCmd = &cobra.Command{
|
||||||
Use: "mails",
|
Use: "mails",
|
||||||
Aliases: []string{"mail"},
|
Aliases: []string{"mail"},
|
||||||
Short: "OnlyOffice Workspace mail — list, read, delete",
|
Short: "OnlyOffice Workspace mail — list, read, draft, delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -20,6 +21,9 @@ func init() {
|
|||||||
mailsCmd.AddCommand(mailsFoldersCmd())
|
mailsCmd.AddCommand(mailsFoldersCmd())
|
||||||
mailsCmd.AddCommand(mailsListCmd())
|
mailsCmd.AddCommand(mailsListCmd())
|
||||||
mailsCmd.AddCommand(mailsGetCmd())
|
mailsCmd.AddCommand(mailsGetCmd())
|
||||||
|
mailsCmd.AddCommand(mailsDraftCmd())
|
||||||
|
mailsCmd.AddCommand(mailsAttachCmd())
|
||||||
|
mailsCmd.AddCommand(mailsDraftInvoiceCmd())
|
||||||
mailsCmd.AddCommand(mailsDeleteCmd())
|
mailsCmd.AddCommand(mailsDeleteCmd())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +130,176 @@ func mailsGetCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mailsDraftCmd() *cobra.Command {
|
||||||
|
var from, to, cc, bcc, subject, body, html string
|
||||||
|
var id int64
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "draft",
|
||||||
|
Short: "Create or update a mail draft (OnlyOffice Mail)",
|
||||||
|
Long: `Save a draft in /addons/mail (PUT /api/2.0/mail/drafts/save).
|
||||||
|
|
||||||
|
oo mails draft --to a@b.com --subject "…" --body "<p>…</p>"
|
||||||
|
oo mails draft --id 123 --to a@b.com --subject "…" --html "<p>…</p>"
|
||||||
|
`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if to == "" {
|
||||||
|
return fmt.Errorf("--to is required")
|
||||||
|
}
|
||||||
|
htmlBody := body
|
||||||
|
if html != "" {
|
||||||
|
htmlBody = html
|
||||||
|
}
|
||||||
|
if htmlBody == "" {
|
||||||
|
return fmt.Errorf("--body or --html is required")
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out, err := c.SaveMailDraft(cmd.Context(), onlyoffice.SaveMailDraftParams{
|
||||||
|
ID: id,
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
Cc: cc,
|
||||||
|
Bcc: bcc,
|
||||||
|
Subject: subject,
|
||||||
|
Body: onlyoffice.PlainTextToMailHTML(htmlBody),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printObject(out)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().Int64Var(&id, "id", 0, "existing draft id (0 = create)")
|
||||||
|
cmd.Flags().StringVar(&from, "from", "", "from address (default: first enabled mailbox)")
|
||||||
|
cmd.Flags().StringVar(&to, "to", "", "recipient (required)")
|
||||||
|
cmd.Flags().StringVar(&cc, "cc", "", "cc")
|
||||||
|
cmd.Flags().StringVar(&bcc, "bcc", "", "bcc")
|
||||||
|
cmd.Flags().StringVar(&subject, "subject", "", "subject")
|
||||||
|
cmd.Flags().StringVar(&body, "body", "", "plain text or HTML body")
|
||||||
|
cmd.Flags().StringVar(&html, "html", "", "HTML body (alias of --body when set)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func mailsAttachCmd() *cobra.Command {
|
||||||
|
var fileID int64
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "attach MESSAGE_ID",
|
||||||
|
Short: "Attach an OnlyOffice Files document to a draft/message",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if fileID <= 0 {
|
||||||
|
return fmt.Errorf("--file-id is required")
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out, err := c.AttachMailDocument(cmd.Context(), args[0], fileID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printObject(out)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().Int64Var(&fileID, "file-id", 0, "OnlyOffice file id (e.g. invoice PDF)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func mailsDraftInvoiceCmd() *cobra.Command {
|
||||||
|
var to, from, subject, body string
|
||||||
|
var invoiceID int
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "draft-invoice",
|
||||||
|
Short: "Create a mail draft with regenerated invoice PDF attached",
|
||||||
|
Long: `Regenerate the CRM invoice PDF, save an OnlyOffice Mail draft, and attach the PDF.
|
||||||
|
|
||||||
|
Does not send. Open /addons/mail/#drafts to review.
|
||||||
|
|
||||||
|
oo mails draft-invoice --invoice 16 --to info@example.com
|
||||||
|
`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if invoiceID <= 0 || to == "" {
|
||||||
|
return fmt.Errorf("--invoice and --to are required")
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pdf, err := c.InvoicePDFFile(cmd.Context(), strconv.Itoa(invoiceID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileID := onlyoffice.Int64FromMap(pdf, "id")
|
||||||
|
if fileID <= 0 {
|
||||||
|
return fmt.Errorf("invoice PDF has no file id: %+v", pdf)
|
||||||
|
}
|
||||||
|
inv, err := c.GetInvoice(cmd.Context(), strconv.Itoa(invoiceID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
number := strings.TrimSpace(fmt.Sprint(inv["number"]))
|
||||||
|
cost := formatInvoiceCostEUR(inv["cost"])
|
||||||
|
if subject == "" {
|
||||||
|
subject = fmt.Sprintf("Rechnung %s — Service package (%s EUR)", number, cost)
|
||||||
|
}
|
||||||
|
if body == "" {
|
||||||
|
body = fmt.Sprintf(`Guten Tag,
|
||||||
|
|
||||||
|
anbei übersende ich die Rechnung %s über %s EUR
|
||||||
|
für das Service package (OS image inkl. Backup, Monitoring, RustDesk,
|
||||||
|
gemeinsamem Datenträger und Netzwerkzugriff).
|
||||||
|
|
||||||
|
Zahlungsziel: 14 Tage netto.
|
||||||
|
Steuerhinweis: siehe Notizen auf der Rechnung (Reverse Charge / § 13b UStG).
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen
|
||||||
|
Example Author`, number, cost)
|
||||||
|
}
|
||||||
|
draft, err := c.SaveMailDraft(cmd.Context(), onlyoffice.SaveMailDraftParams{
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
Subject: subject,
|
||||||
|
Body: onlyoffice.PlainTextToMailHTML(body),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mid := fmt.Sprint(draft["id"])
|
||||||
|
att, err := c.AttachMailDocument(cmd.Context(), mid, fileID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("draft %s created but attach failed: %w", mid, err)
|
||||||
|
}
|
||||||
|
if outputFormat == "json" {
|
||||||
|
printObject(map[string]any{"draft": draft, "attachment": att, "pdfFileId": fileID})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf("draft %s to=%s subject=%q pdfFileId=%d\n", mid, to, subject, fileID)
|
||||||
|
fmt.Printf("open: https://office.example.com/addons/mail/#drafts\n")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().IntVar(&invoiceID, "invoice", 0, "CRM invoice id")
|
||||||
|
cmd.Flags().StringVar(&to, "to", "", "recipient")
|
||||||
|
cmd.Flags().StringVar(&from, "from", "", "from address (default mailbox)")
|
||||||
|
cmd.Flags().StringVar(&subject, "subject", "", "override subject")
|
||||||
|
cmd.Flags().StringVar(&body, "body", "", "override plain-text body")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatInvoiceCostEUR(v any) string {
|
||||||
|
s := strings.TrimSpace(fmt.Sprint(v))
|
||||||
|
s = strings.TrimSuffix(s, ".00")
|
||||||
|
s = strings.TrimSuffix(s, ".0")
|
||||||
|
if s == "" || s == "<nil>" {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func mailsDeleteCmd() *cobra.Command {
|
func mailsDeleteCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "delete ID [ID...]",
|
Use: "delete ID [ID...]",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package onlyoffice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -96,11 +97,156 @@ func (c *Client) GetMailMessage(ctx context.Context, messageID string) (map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveMailMessages deletes messages by id (PUT /api/2.0/mail/messages/remove).
|
// RemoveMailMessages deletes messages by id (PUT /api/2.0/mail/messages/remove).
|
||||||
|
// The API response "response" field may be a number or object; success is HTTP 2xx.
|
||||||
func (c *Client) RemoveMailMessages(ctx context.Context, ids ...int) (map[string]any, error) {
|
func (c *Client) RemoveMailMessages(ctx context.Context, ids ...int) (map[string]any, error) {
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return nil, fmt.Errorf("RemoveMailMessages: at least one id is required")
|
return nil, fmt.Errorf("RemoveMailMessages: at least one id is required")
|
||||||
}
|
}
|
||||||
return c.putJSONObject(ctx, "/api/2.0/mail/messages/remove", map[string]any{"ids": ids})
|
raw, err := c.putJSON(ctx, "/api/2.0/mail/messages/remove", map[string]any{"ids": ids})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := map[string]any{"ok": true, "ids": ids}
|
||||||
|
if resp, err := responseField(raw, "response"); err == nil {
|
||||||
|
var n json.Number
|
||||||
|
if json.Unmarshal(resp, &n) == nil {
|
||||||
|
out["response"] = n.String()
|
||||||
|
} else {
|
||||||
|
var m map[string]any
|
||||||
|
if json.Unmarshal(resp, &m) == nil {
|
||||||
|
out["response"] = m
|
||||||
|
} else {
|
||||||
|
out["response"] = string(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveMailDraftParams creates or updates a draft via PUT /api/2.0/mail/drafts/save.
|
||||||
|
// Id 0 creates a new draft. Body is HTML; the API field name is "body" (not htmlBody).
|
||||||
|
type SaveMailDraftParams struct {
|
||||||
|
ID int64 // 0 = create
|
||||||
|
From string // mailbox address, e.g. eslider@gmail.com
|
||||||
|
To string // comma-separated or single address
|
||||||
|
Cc string
|
||||||
|
Bcc string
|
||||||
|
Subject string
|
||||||
|
Body string // HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveMailDraft saves a draft message. Returns the saved message map (incl. id).
|
||||||
|
func (c *Client) SaveMailDraft(ctx context.Context, p SaveMailDraftParams) (map[string]any, error) {
|
||||||
|
if strings.TrimSpace(p.To) == "" {
|
||||||
|
return nil, fmt.Errorf("SaveMailDraft: to is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.From) == "" {
|
||||||
|
from, err := c.defaultMailFrom(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.From = from
|
||||||
|
}
|
||||||
|
body := map[string]any{
|
||||||
|
"id": p.ID,
|
||||||
|
"from": p.From,
|
||||||
|
"to": p.To,
|
||||||
|
"cc": p.Cc,
|
||||||
|
"bcc": p.Bcc,
|
||||||
|
"subject": p.Subject,
|
||||||
|
"body": p.Body,
|
||||||
|
}
|
||||||
|
return c.putJSONObject(ctx, "/api/2.0/mail/drafts/save", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) defaultMailFrom(ctx context.Context) (string, error) {
|
||||||
|
accounts, err := c.ListMailAccounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var fallback string
|
||||||
|
for _, a := range accounts {
|
||||||
|
email := strings.TrimSpace(stringFromMap(a, "email"))
|
||||||
|
if email == "" || email == "<nil>" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if boolField(a, "enabled") || boolField(a, "isDefault") {
|
||||||
|
return email, nil
|
||||||
|
}
|
||||||
|
if fallback == "" {
|
||||||
|
fallback = email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fallback == "" {
|
||||||
|
return "", fmt.Errorf("SaveMailDraft: from is required (no mailbox on account)")
|
||||||
|
}
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachMailDocument attaches an OnlyOffice Files document to a mail message/draft.
|
||||||
|
// fileID is the Documents/CRM file id (e.g. from InvoicePDFFile).
|
||||||
|
func (c *Client) AttachMailDocument(ctx context.Context, messageID string, fileID int64) (map[string]any, error) {
|
||||||
|
id := strings.TrimSpace(messageID)
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("AttachMailDocument: message id is required")
|
||||||
|
}
|
||||||
|
if fileID <= 0 {
|
||||||
|
return nil, fmt.Errorf("AttachMailDocument: file id is required")
|
||||||
|
}
|
||||||
|
fields := url.Values{}
|
||||||
|
fields.Set("fileId", strconv.FormatInt(fileID, 10))
|
||||||
|
return c.postFormObject(ctx, fmt.Sprintf("/api/2.0/mail/messages/%s/document", url.PathEscape(id)), fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvoicePDFFile regenerates/returns the invoice PDF file metadata (id, title, viewUrl).
|
||||||
|
func (c *Client) InvoicePDFFile(ctx context.Context, invoiceID string) (map[string]any, error) {
|
||||||
|
id := strings.TrimSpace(invoiceID)
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("InvoicePDFFile: invoice id is required")
|
||||||
|
}
|
||||||
|
return c.ResponseObject(ctx, fmt.Sprintf("/api/2.0/crm/invoice/%s/pdf", url.PathEscape(id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlainTextToMailHTML turns plain text into simple HTML paragraphs for drafts.
|
||||||
|
// If s already looks like HTML, it is returned unchanged.
|
||||||
|
func PlainTextToMailHTML(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "<") && strings.Contains(s, ">") {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
parts := strings.Split(s, "\n\n")
|
||||||
|
var b strings.Builder
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString("<p>")
|
||||||
|
b.WriteString(strings.ReplaceAll(p, "\n", "<br/>"))
|
||||||
|
b.WriteString("</p>\n")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64FromMap coerces OnlyOffice numeric id fields (float64/int/string) to int64.
|
||||||
|
func Int64FromMap(m map[string]any, key string) int64 {
|
||||||
|
switch v := m[key].(type) {
|
||||||
|
case float64:
|
||||||
|
return int64(v)
|
||||||
|
case int64:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return int64(v)
|
||||||
|
case string:
|
||||||
|
n, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
||||||
|
return n
|
||||||
|
default:
|
||||||
|
n, _ := strconv.ParseInt(fmt.Sprint(m[key]), 10, 64)
|
||||||
|
return n
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveMailFolder maps a CLI folder name or numeric string to a folder id.
|
// ResolveMailFolder maps a CLI folder name or numeric string to a folder id.
|
||||||
|
|||||||
+24
-1
@@ -1,6 +1,9 @@
|
|||||||
package onlyoffice
|
package onlyoffice
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestResolveMailFolder(t *testing.T) {
|
func TestResolveMailFolder(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -70,3 +73,23 @@ func TestMailMessagesAsTableRows(t *testing.T) {
|
|||||||
t.Fatalf("got %+v", rows[0])
|
t.Fatalf("got %+v", rows[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlainTextToMailHTML(t *testing.T) {
|
||||||
|
got := PlainTextToMailHTML("Hello\n\nWorld\nline2")
|
||||||
|
if !strings.Contains(got, "<p>Hello</p>") || !strings.Contains(got, "World<br/>line2") {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
html := "<p>Already</p>"
|
||||||
|
if PlainTextToMailHTML(html) != html {
|
||||||
|
t.Fatalf("html passthrough failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInt64FromMap(t *testing.T) {
|
||||||
|
if Int64FromMap(map[string]any{"id": float64(99)}, "id") != 99 {
|
||||||
|
t.Fatal("float64")
|
||||||
|
}
|
||||||
|
if Int64FromMap(map[string]any{"id": "42"}, "id") != 42 {
|
||||||
|
t.Fatal("string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user