commit 8a399a9
delthas
·
2025-11-07 12:06:29 +0000 UTC
parent fa9130a
Support opening irc:// links in senpai
13 files changed,
+405,
-33
M
app.go
+159,
-8
1@@ -28,6 +28,7 @@ import (
2 "unicode"
3 "unicode/utf8"
4
5+ "git.sr.ht/~delthas/senpai/varlinkservice"
6 "git.sr.ht/~rockorager/vaxis"
7 "golang.org/x/net/proxy"
8
9@@ -97,6 +98,10 @@ type event struct {
10 content interface{}
11 }
12
13+type linkEvent struct {
14+ link string
15+}
16+
17 type boundKey struct {
18 netID string
19 target string
20@@ -134,8 +139,8 @@ type App struct {
21
22 monitor map[string]map[string]struct{} // set of targets we want to monitor per netID, best-effort. netID->target->{}
23
24- networkLock sync.RWMutex // locks networks
25- networks map[string]struct{} // set of network IDs we want to connect to; to be locked with networkLock
26+ networkLock sync.RWMutex // locks networks
27+ networks map[string]map[string]string // set of network IDs to attributes we want to connect to; to be locked with networkLock
28
29 pendingCompletions map[string][]pendingCompletion
30 pendingCompletionsOff int
31@@ -170,7 +175,7 @@ func NewApp(cfg Config) (app *App, err error) {
32 }
33
34 app = &App{
35- networks: map[string]struct{}{
36+ networks: map[string]map[string]string{
37 "": {}, // add the master network by default
38 },
39 pendingCompletions: make(map[string][]pendingCompletion),
40@@ -385,6 +390,41 @@ func (app *App) wantsNetwork(netID string) bool {
41 return ok
42 }
43
44+func (app *App) findNetworkByLink(link string) (host string, target string, netID string) {
45+ // links often do not contain encoded #
46+ link = strings.ReplaceAll(link, "#", "%23")
47+
48+ u, err := url.Parse(link)
49+ if err != nil {
50+ return "", "", ""
51+ }
52+ host, port, err := net.SplitHostPort(u.Host)
53+ if err != nil {
54+ host = u.Host
55+ port = "6697"
56+ }
57+ target = strings.TrimLeft(u.Path, "/")
58+ if i := strings.IndexAny(target, "/,"); i >= 0 {
59+ target = target[:i]
60+ }
61+ app.networkLock.Lock()
62+ defer app.networkLock.Unlock()
63+ for netID, attrs := range app.networks {
64+ if attrs["host"] != host {
65+ continue
66+ }
67+ p := attrs["port"]
68+ if p == "" {
69+ p = "6697"
70+ }
71+ if p != port {
72+ continue
73+ }
74+ return u.Host, target, netID
75+ }
76+ return u.Host, target, ""
77+}
78+
79 // ircLoop maintains a connection to the IRC server by connecting and then
80 // forwarding IRC events to app.events repeatedly.
81 func (app *App) ircLoop(netID string) {
82@@ -645,6 +685,38 @@ func (app *App) handleUIEvent(ev interface{}) bool {
83 } else {
84 app.uploadingProgress = &ev.Progress
85 }
86+ case *events.EventOpenLink:
87+ host, target, netID := app.findNetworkByLink(ev.Link)
88+ if netID != "" {
89+ if cNetID, _ := app.win.CurrentBuffer(); cNetID != netID {
90+ app.win.JumpBufferNetwork(netID, "")
91+ }
92+ if target == "" {
93+ break
94+ }
95+ if !app.win.JumpBufferNetwork(netID, target) {
96+ if s := app.sessions[netID]; s != nil && s.IsChannel(target) {
97+ cNetID, cTarget := app.win.CurrentBuffer()
98+ app.win.AddLine(cNetID, cTarget, ui.Line{
99+ At: time.Now(),
100+ Head: "--",
101+ Body: ui.PlainString("An IRC link of a new channel was opened. Enter to add and join that channel."),
102+ })
103+ app.win.InputSet(fmt.Sprintf("/join %v", target))
104+ } else {
105+ i, _ := app.addUserBuffer(netID, target, time.Time{})
106+ app.win.JumpBufferIndex(i)
107+ }
108+ }
109+ } else {
110+ netID, target := app.win.CurrentBuffer()
111+ app.win.AddLine(netID, target, ui.Line{
112+ At: time.Now(),
113+ Head: "--",
114+ Body: ui.PlainString("An IRC link of a new network was opened. Enter to add and join that network."),
115+ })
116+ app.win.InputSet(fmt.Sprintf("/bouncer network create -addr %q", host))
117+ }
118 default:
119 // TODO: missing event types
120 }
121@@ -1456,6 +1528,37 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
122 // TODO: batch MONITOR +
123 s.MonitorAdd(target)
124 }
125+
126+ if netID == "" || app.cfg.OpenLink == "" {
127+ break
128+ }
129+ _, target, foundNetID := app.findNetworkByLink(app.cfg.OpenLink)
130+ if netID != foundNetID {
131+ break
132+ }
133+ app.cfg.OpenLink = ""
134+ app.lastNetID = ""
135+ app.lastBuffer = ""
136+ if cNetID, _ := app.win.CurrentBuffer(); cNetID != netID {
137+ app.win.JumpBufferNetwork(netID, "")
138+ }
139+ if target == "" {
140+ break
141+ }
142+ if !app.win.JumpBufferNetwork(netID, target) {
143+ if s.IsChannel(target) {
144+ cNetID, cTarget := app.win.CurrentBuffer()
145+ app.win.AddLine(cNetID, cTarget, ui.Line{
146+ At: time.Now(),
147+ Head: "--",
148+ Body: ui.PlainString("An IRC link of a new channel was opened. Enter to add and join that channel."),
149+ })
150+ app.win.InputSet(fmt.Sprintf("/join %v", target))
151+ } else {
152+ i, _ := app.addUserBuffer(netID, target, time.Time{})
153+ app.win.JumpBufferIndex(i)
154+ }
155+ }
156 case irc.SelfNickEvent:
157 if !app.cfg.StatusEnabled {
158 break
159@@ -1707,11 +1810,17 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
160 }
161 case irc.BouncerNetworkEvent:
162 if !ev.Delete {
163- _, added := app.win.AddBuffer(ev.ID, ev.Name, "")
164+ app.networkLock.Lock()
165+ if _, ok := app.networks[ev.ID]; !ok {
166+ app.networks[ev.ID] = ev.Attrs
167+ } else {
168+ for k, v := range ev.Attrs {
169+ app.networks[ev.ID][k] = v
170+ }
171+ }
172+ app.networkLock.Unlock()
173+ _, added := app.win.AddBuffer(ev.ID, ev.Attrs["name"], "")
174 if added {
175- app.networkLock.Lock()
176- app.networks[ev.ID] = struct{}{}
177- app.networkLock.Unlock()
178 go app.ircLoop(ev.ID)
179 }
180 } else {
181@@ -1727,6 +1836,39 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
182 }
183 app.win.RemoveNetworkBuffers(ev.ID)
184 }
185+ case irc.BouncerNetworkListEvent:
186+ for _, ev := range ev {
187+ app.networkLock.Lock()
188+ if _, ok := app.networks[ev.ID]; !ok {
189+ app.networks[ev.ID] = ev.Attrs
190+ } else {
191+ for k, v := range ev.Attrs {
192+ app.networks[ev.ID][k] = v
193+ }
194+ }
195+ app.networkLock.Unlock()
196+ _, added := app.win.AddBuffer(ev.ID, ev.Attrs["name"], "")
197+ if added {
198+ go app.ircLoop(ev.ID)
199+ }
200+ }
201+
202+ if app.cfg.OpenLink == "" {
203+ break
204+ }
205+ host, _, foundNetID := app.findNetworkByLink(app.cfg.OpenLink)
206+ if foundNetID != "" {
207+ // We will handle the link in the session for that network.
208+ break
209+ }
210+ app.cfg.OpenLink = ""
211+ netID, target := app.win.CurrentBuffer()
212+ app.win.AddLine(netID, target, ui.Line{
213+ At: time.Now(),
214+ Head: "--",
215+ Body: ui.PlainString("An IRC link of a new network was opened. Enter to add and join that network."),
216+ })
217+ app.win.InputSet(fmt.Sprintf("/bouncer network create -addr %q", host))
218 case irc.ListEvent:
219 for _, item := range ev {
220 text := fmt.Sprintf("There are %4s users on channel %s", item.Count, item.Channel)
221@@ -1757,7 +1899,6 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
222 Foreground: app.cfg.Colors.Status,
223 }),
224 })
225- return
226 case irc.ErrorEvent:
227 var head string
228 var body string
229@@ -2444,6 +2585,16 @@ func (app *App) addUserBuffer(netID, buffer string, t time.Time) (i int, added b
230 return
231 }
232
233+func (app *App) OpenLink(in *varlinkservice.OpenLinkIn) (*varlinkservice.OpenLinkOut, error) {
234+ app.postEvent(event{
235+ src: "*",
236+ content: &events.EventOpenLink{
237+ Link: in.Link,
238+ },
239+ })
240+ return nil, nil
241+}
242+
243 func keyNameMatch(name string) *keyMatch {
244 parts := strings.Split(name, "+")
245 mods := parts[:len(parts)-1]
+83,
-0
1@@ -6,16 +6,21 @@ import (
2 "fmt"
3 "io"
4 "math/rand"
5+ "net"
6+ "net/url"
7 "os"
8 "os/signal"
9 "os/user"
10 "path"
11+ "path/filepath"
12 "runtime"
13 "strings"
14 "syscall"
15 "time"
16
17 "git.sr.ht/~delthas/senpai"
18+ "git.sr.ht/~delthas/senpai/varlinkservice"
19+ "github.com/emersion/go-varlink"
20 )
21
22 func main() {
23@@ -40,6 +45,26 @@ func main() {
24 return
25 }
26
27+ socketDir := os.Getenv("XDG_RUNTIME_DIR")
28+ if socketDir == "" {
29+ socketDir = os.TempDir()
30+ }
31+ socketDir = filepath.Join(socketDir, "senpai")
32+
33+ var link string
34+ if _, err := url.Parse(flag.Arg(0)); err == nil {
35+ link = flag.Arg(0)
36+ }
37+ if link != "" {
38+ ok, err := sendOpenLink(socketDir, link)
39+ if err != nil {
40+ fmt.Fprintf(os.Stderr, "failed to send open link to another instance: %v\n", err)
41+ } else if ok {
42+ // Sent link open command to another instance, exit.
43+ return
44+ }
45+ }
46+
47 var previousConfigPath string
48 if configPath == "" {
49 configDir, err := os.UserConfigDir()
50@@ -171,6 +196,7 @@ func main() {
51 fmt.Fprintf(os.Stderr, "\n")
52 }
53
54+ cfg.OpenLink = link
55 cfg.Debug = cfg.Debug || debug
56 if nickname != "" {
57 cfg.Nick = nickname
58@@ -197,6 +223,14 @@ func main() {
59 app.Close()
60 }()
61
62+ if cfg.LocalIntegrations {
63+ socketPath := filepath.Join(socketDir, fmt.Sprintf("%v.sock", os.Getpid()))
64+ if err := listenVarlink(socketPath, app); err != nil {
65+ fmt.Fprintf(os.Stderr, "failed to send open link to another instance: %v\n", err)
66+ }
67+ defer os.Remove(socketPath)
68+ }
69+
70 app.Run()
71 app.Close()
72 if !cfg.Transient {
73@@ -309,3 +343,52 @@ func writeLastStamp(app *senpai.App) {
74 fmt.Fprintf(os.Stderr, "failed to write last stamp at %q: %s\n", lastStampPath, err)
75 }
76 }
77+
78+func sendOpenLink(socketDir string, link string) (ok bool, err error) {
79+ es, err := os.ReadDir(socketDir)
80+ if os.IsNotExist(err) {
81+ return false, nil
82+ }
83+ if err != nil {
84+ return false, err
85+ }
86+ for _, e := range es {
87+ if filepath.Ext(e.Name()) != ".sock" {
88+ continue
89+ }
90+ conn, err := net.Dial("unix", filepath.Join(socketDir, e.Name()))
91+ if err != nil {
92+ return false, err
93+ }
94+ c := varlinkservice.Client{Client: varlink.NewClient(conn)}
95+ defer c.Close()
96+ if _, err := c.OpenLink(&varlinkservice.OpenLinkIn{Link: link}); err != nil {
97+ return false, err
98+ }
99+ return true, nil
100+ }
101+ return false, nil
102+}
103+
104+func listenVarlink(socketPath string, backend varlinkservice.Backend) error {
105+ if err := os.MkdirAll(filepath.Dir(socketPath), 0700); err != nil {
106+ return err
107+ }
108+ if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
109+ return err
110+ }
111+ l, err := net.Listen("unix", socketPath)
112+ if err != nil {
113+ return err
114+ }
115+ server := varlink.NewServer()
116+ server.Handler = varlinkservice.Handler{
117+ Backend: backend,
118+ }
119+ go func() {
120+ if err := server.Serve(l); err != nil {
121+ fmt.Fprintf(os.Stderr, "failed to serve varlink server: %v\n", err)
122+ }
123+ }()
124+ return nil
125+}
+1,
-0
1@@ -115,6 +115,7 @@ type Config struct {
2
3 Colors ui.ConfigColors
4
5+ OpenLink string
6 Debug bool
7 Transient bool
8 LocalIntegrations bool
+2,
-1
1@@ -8,6 +8,7 @@ Keywords=Chat;IRC;IM;Messaging
2 Categories=Network;InstantMessaging;Chat;IRCClient;ConsoleOnly
3
4 Type=Application
5+MimeType=x-scheme-handler/irc;x-scheme-handler/ircs
6 Icon=senpai
7 Terminal=true
8-Exec=senpai
9+Exec=senpai %u
+4,
-1
1@@ -6,7 +6,7 @@ senpai - your everyday IRC student
2
3 # SYNOPSIS
4
5-*senpai* [options...]
6+*senpai* [options...] [link]
7
8 # OPTIONS
9
10@@ -24,6 +24,9 @@ senpai - your everyday IRC student
11 *-version*
12 Display version info.
13
14+senpai can also be opened with an _irc://_ link to a server or target. In that
15+case, the link is opened in an existing instance if available.
16+
17 # DESCRIPTION
18
19 senpai is an IRC client made for bouncers. It supports the newest IRC
+4,
-0
1@@ -45,3 +45,7 @@ type EventFileUpload struct {
2 Location string
3 Error string
4 }
5+
6+type EventOpenLink struct {
7+ Link string
8+}
M
go.mod
+3,
-1
1@@ -9,17 +9,19 @@ require (
2 github.com/delthas/go-libnp v0.1.0
3 github.com/delthas/go-localeinfo v0.1.0
4 github.com/disintegration/imaging v1.6.2
5+ github.com/emersion/go-varlink v0.0.0-20251006073247-e7ad9f7950c7
6 github.com/godbus/dbus/v5 v5.1.0
7 github.com/rivo/uniseg v0.4.7
8+ golang.org/x/image v0.24.0
9 golang.org/x/net v0.35.0
10 golang.org/x/time v0.10.0
11 mvdan.cc/xurls/v2 v2.5.0
12 )
13
14 require (
15+ github.com/dave/jennifer v1.7.1 // indirect
16 github.com/mattn/go-runewidth v0.0.16 // indirect
17 github.com/mattn/go-sixel v0.0.5 // indirect
18 github.com/soniakeys/quant v1.0.0 // indirect
19- golang.org/x/image v0.24.0 // indirect
20 golang.org/x/sys v0.30.0 // indirect
21 )
M
go.sum
+4,
-0
1@@ -4,6 +4,8 @@ git.sr.ht/~rockorager/vaxis v0.13.0 h1:+F3ze1t4X5x87QEsyn/b9b9pWXw9MDqoHynoR/PHq
2 git.sr.ht/~rockorager/vaxis v0.13.0/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
3 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
4 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
5+github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
6+github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
7 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 github.com/delthas/go-libnp v0.1.0 h1:4YmxYd6yBI1gDzSeqCS7+0wrHcYPPo54FrTaFIbEd4U=
10@@ -12,6 +14,8 @@ github.com/delthas/go-localeinfo v0.1.0 h1:2tN27F8QcWVVn218n6raAYa5sL7gDLVGEw5x3
11 github.com/delthas/go-localeinfo v0.1.0/go.mod h1:sG54BxlyQgIskYURLrg7mvhoGBe0Qq12DNtYRALwNa4=
12 github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
13 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
14+github.com/emersion/go-varlink v0.0.0-20251006073247-e7ad9f7950c7 h1:GtfymsmxgcEdOuYLm5CR6VS5pTEXfUKkQDp/wRvvyA8=
15+github.com/emersion/go-varlink v0.0.0-20251006073247-e7ad9f7950c7/go.mod h1:0/V0Ta8VUzKRLXUtyZS49soMI93Taqlm63wX5nx6YEo=
16 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
17 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
18 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+3,
-1
1@@ -126,6 +126,8 @@ type MetadataChangeEvent struct {
2
3 type BouncerNetworkEvent struct {
4 ID string
5- Name string
6+ Attrs map[string]string
7 Delete bool
8 }
9+
10+type BouncerNetworkListEvent []BouncerNetworkEvent
+38,
-21
1@@ -129,14 +129,15 @@ type Session struct {
2 typings *Typings // incoming typing notifications.
3 typingStamps map[string]typingStamp // user typing instants.
4
5- nick string
6- nickCf string // casemapped nickname.
7- user string
8- real string
9- acct string
10- host string
11- netID string
12- auth SASLClient
13+ nick string
14+ nickCf string // casemapped nickname.
15+ user string
16+ real string
17+ acct string
18+ host string
19+ netID string
20+ netAttrs map[string]string
21+ auth SASLClient
22
23 availableCaps map[string]string
24 enabledCaps map[string]struct{}
25@@ -157,17 +158,19 @@ type Session struct {
26 listMask bool
27 upload string
28
29- users map[string]*User // known users.
30- channels map[string]Channel // joined channels.
31- metadata map[string]Metadata // known target metadata.
32- chBatches map[string]HistoryEvent // channel history batches being processed.
33- chReqs map[string]struct{} // set of targets for which history is currently requested.
34- targetsBatchID string // ID of the channel history targets batch being processed.
35- targetsBatch HistoryTargetsEvent // channel history targets batch being processed.
36- searchBatchID string // ID of the search targets batch being processed.
37- searchBatch SearchEvent // search batch being processed.
38- monitors map[string]struct{} // set of users we want to monitor (and keep even if they are disconnected).
39- pendingList ListEvent // current list response being received (flushed on list end).
40+ users map[string]*User // known users.
41+ channels map[string]Channel // joined channels.
42+ metadata map[string]Metadata // known target metadata.
43+ chBatches map[string]HistoryEvent // channel history batches being processed.
44+ chReqs map[string]struct{} // set of targets for which history is currently requested.
45+ targetsBatchID string // ID of the channel history targets batch being processed.
46+ targetsBatch HistoryTargetsEvent // channel history targets batch being processed.
47+ searchBatchID string // ID of the search targets batch being processed.
48+ searchBatch SearchEvent // search batch being processed.
49+ bouncerNetworksBatchID string // ID of the bouncer network batch being processed.
50+ bouncerNetworksBatch BouncerNetworkListEvent // bouncer network batch being processed.
51+ monitors map[string]struct{} // set of users we want to monitor (and keep even if they are disconnected).
52+ pendingList ListEvent // current list response being received (flushed on list end).
53
54 pendingChannels map[string]time.Time // set of join requests stamps for channels.
55
56@@ -783,6 +786,15 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
57 s.searchBatch.Messages = append(s.searchBatch.Messages, ev)
58 return nil, nil
59 }
60+ } else if id == s.bouncerNetworksBatchID {
61+ ev, err := s.handleMessageRegistered(msg, true)
62+ if err != nil {
63+ return nil, err
64+ }
65+ if ev, ok := ev.(BouncerNetworkEvent); ok {
66+ s.bouncerNetworksBatch = append(s.bouncerNetworksBatch, ev)
67+ return nil, nil
68+ }
69 } else if b, ok := s.chBatches[id]; ok {
70 ev, err := s.handleMessageRegistered(msg, true)
71 if err != nil {
72@@ -1444,6 +1456,9 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
73 case "soju.im/search":
74 s.searchBatchID = id
75 s.searchBatch = SearchEvent{}
76+ case "soju.im/bouncer-networks":
77+ s.bouncerNetworksBatchID = id
78+ s.bouncerNetworksBatch = BouncerNetworkListEvent{}
79 }
80 } else {
81 if b, ok := s.chBatches[id]; ok {
82@@ -1457,6 +1472,9 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
83 } else if s.searchBatchID == id {
84 s.searchBatchID = ""
85 return s.searchBatch, nil
86+ } else if s.bouncerNetworksBatchID == id {
87+ s.bouncerNetworksBatchID = ""
88+ return s.bouncerNetworksBatch, nil
89 }
90 }
91 case "NICK":
92@@ -1564,8 +1582,7 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
93 ID: id,
94 }
95 if msg.Params[2] != "*" {
96- attrs := parseTags(msg.Params[2])
97- event.Name = attrs["name"]
98+ event.Attrs = parseTags(msg.Params[2])
99 } else {
100 event.Delete = true
101 }
+92,
-0
1@@ -0,0 +1,92 @@
2+// Code generated by go-varlink/varlinkgen. DO NOT EDIT.
3+
4+package varlinkservice
5+
6+import (
7+ "encoding/json"
8+
9+ govarlink "github.com/emersion/go-varlink"
10+)
11+
12+type OpenLinkIn struct {
13+ Link string `json:"link"`
14+}
15+type OpenLinkOut struct{}
16+
17+type Client struct {
18+ *govarlink.Client
19+}
20+
21+func unmarshalError(err error) error {
22+ verr, ok := err.(*govarlink.ClientError)
23+ if !ok {
24+ return err
25+ }
26+ var v error
27+ switch verr.Name {
28+ default:
29+ return err
30+ }
31+ if err := json.Unmarshal(verr.Parameters, v); err != nil {
32+ return err
33+ }
34+ return v
35+}
36+func (c Client) OpenLink(in *OpenLinkIn) (*OpenLinkOut, error) {
37+ if in == nil {
38+ in = new(OpenLinkIn)
39+ }
40+ out := new(OpenLinkOut)
41+ err := c.Client.Do("fr.delthas.senpai.OpenLink", in, out)
42+ return out, unmarshalError(err)
43+}
44+
45+type Backend interface {
46+ OpenLink(*OpenLinkIn) (*OpenLinkOut, error)
47+}
48+
49+type Handler struct {
50+ Backend Backend
51+}
52+
53+func marshalError(err error) error {
54+ var name string
55+ switch err.(type) {
56+ default:
57+ return err
58+ }
59+ return &govarlink.ServerError{
60+ Name: name,
61+ Parameters: err,
62+ }
63+}
64+func (h Handler) HandleVarlink(call *govarlink.ServerCall, req *govarlink.ServerRequest) error {
65+ var (
66+ out interface{}
67+ err error
68+ )
69+ switch req.Method {
70+ case "fr.delthas.senpai.OpenLink":
71+ in := new(OpenLinkIn)
72+ if err := json.Unmarshal(req.Parameters, in); err != nil {
73+ return err
74+ }
75+ out, err = h.Backend.OpenLink(in)
76+ default:
77+ err = &govarlink.ServerError{
78+ Name: "org.varlink.service.MethodNotFound",
79+ Parameters: map[string]string{"method": req.Method},
80+ }
81+ }
82+ if err != nil {
83+ return marshalError(err)
84+ }
85+ return call.CloseWithReply(out)
86+}
87+
88+func (h Handler) Register(reg *govarlink.Registry) {
89+ reg.Add(&govarlink.RegistryInterface{
90+ Definition: "interface fr.delthas.senpai\n\nmethod OpenLink(link: string) -> ()\n",
91+ Name: "fr.delthas.senpai",
92+ }, h)
93+}
1@@ -0,0 +1,3 @@
2+interface fr.delthas.senpai
3+
4+method OpenLink(link: string) -> ()
+9,
-0
1@@ -0,0 +1,9 @@
2+//go:build generate
3+
4+package varlinkservice
5+
6+import (
7+ _ "github.com/emersion/go-varlink/cmd/varlinkgen"
8+)
9+
10+//go:generate go run github.com/emersion/go-varlink/cmd/varlinkgen -i fr.delthas.senpai.varlink