Repository navigation
Expand file tree
/
Copy patherrors.go
More file actions
105 lines (98 loc) · 2.99 KB
/
Copy patherrors.go
File metadata and controls
105 lines (98 loc) · 2.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package main
import (
"cmp"
"context"
"errors"
"fmt"
"log/slog"
"maps"
"net"
"net/http"
"net/url"
"golang.org/x/oauth2"
"google.golang.org/api/chat/v1"
"google.golang.org/api/googleapi"
)
// describeErr says what went wrong in words for the status line. The log
// keeps the full error.
func describeErr(err error) string {
if re, ok := errors.AsType[*oauth2.RetrieveError](err); ok && re.ErrorCode == "invalid_grant" {
return "your login expired or was revoked: /logout, then start mutter to log in again"
}
if ge, ok := errors.AsType[*googleapi.Error](err); ok {
switch {
case ge.Code == http.StatusTooManyRequests:
return "Google is rate limiting mutter, try again in a minute"
case ge.Code == http.StatusUnauthorized:
return "Google rejected the login: /logout, then start mutter to log in again"
case ge.Code >= http.StatusInternalServerError:
return fmt.Sprintf("Google had a problem (%d), try again", ge.Code)
}
return fmt.Sprintf("Google refused it (%d): %s", ge.Code, clean(cmp.Or(ge.Message, http.StatusText(ge.Code))))
}
if offline(err) {
return "can't reach Google, check the network"
}
return err.Error()
}
// offline reports whether err means Google couldn't be reached, as opposed
// to Google answering with an error.
func offline(err error) bool {
if _, ok := errors.AsType[*oauth2.RetrieveError](err); ok {
return false
}
if _, ok := errors.AsType[*googleapi.Error](err); ok {
return false
}
_, isURL := errors.AsType[*url.Error](err)
_, isNet := errors.AsType[net.Error](err)
return isURL || isNet || errors.Is(err, context.DeadlineExceeded)
}
// sendFailedMsg carries a message that didn't go out, so it isn't lost.
type sendFailedMsg struct {
err error
key string // draftKey of the context it was written in
text string
mentions map[string]string
quote *chat.Message
editing *chat.Message
files []pending
}
// unsent returns a failed message to the input when its context is still
// open, or keeps it as that context's draft. Files and quotes only survive
// in the input.
func (m *model) unsent(msg sendFailedMsg) {
slog.Warn("send failed", "err", msg.err)
why := describeErr(msg.err)
if m.draftKey() != msg.key {
if msg.text != "" && msg.editing == nil {
d := m.drafts[msg.key]
d.Text = joinLines(msg.text, d.Text)
d.Mentions = maps.Clone(msg.mentions)
m.drafts[msg.key] = d
m.writeDrafts()
why += " · not sent, kept as a draft where you wrote it"
} else {
why += " · not sent"
}
m.status = errStyle.Render(why)
return
}
m.ta.SetValue(joinLines(msg.text, m.ta.Value()))
maps.Copy(m.mentions, msg.mentions)
m.pending = append(msg.files, m.pending...)
m.quoting = cmp.Or(m.quoting, msg.quote)
m.editing = cmp.Or(m.editing, msg.editing)
m.status = errStyle.Render(why + " · not sent, it's back in the input")
m.render()
}
// joinLines joins the non-empty texts with newlines.
func joinLines(a, b string) string {
switch {
case a == "":
return b
case b == "":
return a
}
return a + "\n" + b
}