source: code/trunk/contrib/znc-import.go@ 377

Last change on this file since 377 was 357, checked in by contact, 5 years ago

contrib/znc-import: new utility

Allows populating the soju database from a ZNC config file.

File size: 10.1 KB
RevLine 
[357]1package main
2
3import (
4 "bufio"
5 "flag"
6 "fmt"
7 "io"
8 "log"
9 "net/url"
10 "os"
11 "strings"
12 "unicode"
13
14 "git.sr.ht/~emersion/soju"
15 "git.sr.ht/~emersion/soju/config"
16)
17
18const usage = `usage: znc-import [options...] <znc config path>
19
20Imports configuration from a ZNC file. Users and networks are merged if they
21already exist in the soju database. ZNC settings overwrite existing soju
22settings.
23
24Options:
25
26 -help Show this help message
27 -config <path> Path to soju config file
28 -user <username> Limit import to username (may be specified multiple times)
29 -network <name> Limit import to network (may be specified multiple times)
30`
31
32func init() {
33 flag.Usage = func() {
34 fmt.Fprintf(flag.CommandLine.Output(), usage)
35 }
36}
37
38func main() {
39 var configPath string
40 users := make(map[string]bool)
41 networks := make(map[string]bool)
42 flag.StringVar(&configPath, "config", "", "path to configuration file")
43 flag.Var((*stringSetFlag)(&users), "user", "")
44 flag.Var((*stringSetFlag)(&networks), "network", "")
45 flag.Parse()
46
47 zncPath := flag.Arg(0)
48 if zncPath == "" {
49 flag.Usage()
50 os.Exit(1)
51 }
52
53 var cfg *config.Server
54 if configPath != "" {
55 var err error
56 cfg, err = config.Load(configPath)
57 if err != nil {
58 log.Fatalf("failed to load config file: %v", err)
59 }
60 } else {
61 cfg = config.Defaults()
62 }
63
64 db, err := soju.OpenSQLDB(cfg.SQLDriver, cfg.SQLSource)
65 if err != nil {
66 log.Fatalf("failed to open database: %v", err)
67 }
68 defer db.Close()
69
70 f, err := os.Open(zncPath)
71 if err != nil {
72 log.Fatalf("failed to open ZNC configuration file: %v", err)
73 }
74 defer f.Close()
75
76 zp := zncParser{bufio.NewReader(f), 1}
77 root, err := zp.sectionBody("", "")
78 if err != nil {
79 log.Fatalf("failed to parse %q: line %v: %v", zncPath, zp.line, err)
80 }
81
82 l, err := db.ListUsers()
83 if err != nil {
84 log.Fatalf("failed to list users in DB: %v", err)
85 }
86 existingUsers := make(map[string]*soju.User, len(l))
87 for i, u := range l {
88 existingUsers[u.Username] = &l[i]
89 }
90
91 usersCreated := 0
92 usersImported := 0
93 networksImported := 0
94 channelsImported := 0
95 root.ForEach("User", func(section *zncSection) {
96 username := section.Name
97 if len(users) > 0 && !users[username] {
98 return
99 }
100 usersImported++
101
102 u, ok := existingUsers[username]
103 if ok {
104 log.Printf("user %q: updating existing user", username)
105 } else {
106 // "!!" is an invalid crypt format, thus disables password auth
107 u = &soju.User{Username: username, Password: "!!"}
108 usersCreated++
109 log.Printf("user %q: creating new user", username)
110 }
111
112 u.Admin = section.Values.Get("Admin") == "true"
113
114 if err := db.StoreUser(u); err != nil {
115 log.Fatalf("failed to store user %q: %v", username, err)
116 }
117
118 l, err := db.ListNetworks(username)
119 if err != nil {
120 log.Fatalf("failed to list networks for user %q: %v", username, err)
121 }
122 existingNetworks := make(map[string]*soju.Network, len(l))
123 for i, n := range l {
124 existingNetworks[n.GetName()] = &l[i]
125 }
126
127 nick := section.Values.Get("Nick")
128 realname := section.Values.Get("RealName")
129 ident := section.Values.Get("Ident")
130
131 section.ForEach("Network", func(section *zncSection) {
132 netName := section.Name
133 if len(networks) > 0 && !networks[netName] {
134 return
135 }
136 networksImported++
137
138 logPrefix := fmt.Sprintf("user %q: network %q: ", username, netName)
139 logger := log.New(os.Stderr, logPrefix, log.LstdFlags|log.Lmsgprefix)
140
141 netNick := section.Values.Get("Nick")
142 if netNick == "" {
143 netNick = nick
144 }
145 netRealname := section.Values.Get("RealName")
146 if netRealname == "" {
147 netRealname = realname
148 }
149 netIdent := section.Values.Get("Ident")
150 if netIdent == "" {
151 netIdent = ident
152 }
153
154 for _, name := range section.Values["LoadModule"] {
155 switch name {
156 case "sasl":
157 logger.Printf("warning: SASL credentials not imported")
158 case "nickserv":
159 logger.Printf("warning: NickServ credentials not imported")
160 case "perform":
161 logger.Printf("warning: \"perform\" plugin commands not imported")
162 }
163 }
164
165 u, pass, err := importNetworkServer(section.Values.Get("Server"))
166 if err != nil {
167 logger.Fatalf("failed to import server %q: %v", section.Values.Get("Server"), err)
168 }
169
170 n, ok := existingNetworks[netName]
171 if ok {
172 logger.Printf("updating existing network")
173 } else {
174 n = &soju.Network{Name: netName}
175 logger.Printf("creating new network")
176 }
177
178 n.Addr = u.String()
179 n.Nick = netNick
180 n.Username = netIdent
181 n.Realname = netRealname
182 n.Pass = pass
183
184 if err := db.StoreNetwork(username, n); err != nil {
185 logger.Fatalf("failed to store network: %v", err)
186 }
187
188 l, err := db.ListChannels(n.ID)
189 if err != nil {
190 logger.Fatalf("failed to list channels: %v", err)
191 }
192 existingChannels := make(map[string]*soju.Channel, len(l))
193 for i, ch := range l {
194 existingChannels[ch.Name] = &l[i]
195 }
196
197 section.ForEach("Chan", func(section *zncSection) {
198 chName := section.Name
199
200 if section.Values.Get("Disabled") == "true" {
201 logger.Printf("skipping import of disabled channel %q", chName)
202 return
203 }
204
205 channelsImported++
206
207 ch, ok := existingChannels[chName]
208 if ok {
209 logger.Printf("channel %q: updating existing channel", chName)
210 } else {
211 ch = &soju.Channel{Name: chName}
212 logger.Printf("channel %q: creating new channel", chName)
213 }
214
215 ch.Key = section.Values.Get("Key")
216 ch.Detached = section.Values.Get("Detached") == "true"
217
218 if err := db.StoreChannel(n.ID, ch); err != nil {
219 logger.Printf("channel %q: failed to store channel: %v", chName, err)
220 }
221 })
222 })
223 })
224
225 if err := db.Close(); err != nil {
226 log.Printf("failed to close database: %v", err)
227 }
228
229 if usersCreated > 0 {
230 log.Printf("warning: user passwords haven't been imported, please set them with `sojuctl change-password <username>`")
231 }
232
233 log.Printf("imported %v users, %v networks and %v channels", usersImported, networksImported, channelsImported)
234}
235
236func importNetworkServer(s string) (u *url.URL, pass string, err error) {
237 parts := strings.Fields(s)
238 if len(parts) < 2 {
239 return nil, "", fmt.Errorf("expected space-separated host and port")
240 }
241
242 scheme := "irc+insecure"
243 host := parts[0]
244 port := parts[1]
245 if strings.HasPrefix(port, "+") {
246 port = port[1:]
247 scheme = "ircs"
248 }
249
250 if len(parts) > 2 {
251 pass = parts[2]
252 }
253
254 u = &url.URL{
255 Scheme: scheme,
256 Host: host + ":" + port,
257 }
258 return u, pass, nil
259}
260
261type zncSection struct {
262 Type string
263 Name string
264 Values zncValues
265 Children []zncSection
266}
267
268func (s *zncSection) ForEach(typ string, f func(*zncSection)) {
269 for _, section := range s.Children {
270 if section.Type == typ {
271 f(&section)
272 }
273 }
274}
275
276type zncValues map[string][]string
277
278func (zv zncValues) Get(k string) string {
279 if len(zv[k]) == 0 {
280 return ""
281 }
282 return zv[k][0]
283}
284
285type zncParser struct {
286 br *bufio.Reader
287 line int
288}
289
290func (zp *zncParser) readByte() (byte, error) {
291 b, err := zp.br.ReadByte()
292 if b == '\n' {
293 zp.line++
294 }
295 return b, err
296}
297
298func (zp *zncParser) readRune() (rune, int, error) {
299 r, n, err := zp.br.ReadRune()
300 if r == '\n' {
301 zp.line++
302 }
303 return r, n, err
304}
305
306func (zp *zncParser) sectionBody(typ, name string) (*zncSection, error) {
307 section := &zncSection{Type: typ, Name: name, Values: make(zncValues)}
308
309Loop:
310 for {
311 if err := zp.skipSpace(); err != nil {
312 return nil, err
313 }
314
315 b, err := zp.br.Peek(2)
316 if err == io.EOF {
317 break
318 } else if err != nil {
319 return nil, err
320 }
321
322 switch b[0] {
323 case '<':
324 if b[1] == '/' {
325 break Loop
326 } else {
327 childType, childName, err := zp.sectionHeader()
328 if err != nil {
329 return nil, err
330 }
331 child, err := zp.sectionBody(childType, childName)
332 if err != nil {
333 return nil, err
334 }
335 if footerType, err := zp.sectionFooter(); err != nil {
336 return nil, err
337 } else if footerType != childType {
338 return nil, fmt.Errorf("invalid section footer: expected type %q, got %q", childType, footerType)
339 }
340 section.Children = append(section.Children, *child)
341 }
342 case '/':
343 if b[1] == '/' {
344 if err := zp.skipComment(); err != nil {
345 return nil, err
346 }
347 break
348 }
349 fallthrough
350 default:
351 k, v, err := zp.keyValuePair()
352 if err != nil {
353 return nil, err
354 }
355 section.Values[k] = append(section.Values[k], v)
356 }
357 }
358
359 return section, nil
360}
361
362func (zp *zncParser) skipSpace() error {
363 for {
364 r, _, err := zp.readRune()
365 if err == io.EOF {
366 return nil
367 } else if err != nil {
368 return err
369 }
370
371 if !unicode.IsSpace(r) {
372 zp.br.UnreadRune()
373 return nil
374 }
375 }
376}
377
378func (zp *zncParser) skipComment() error {
379 if err := zp.expectRune('/'); err != nil {
380 return err
381 }
382 if err := zp.expectRune('/'); err != nil {
383 return err
384 }
385
386 for {
387 b, err := zp.readByte()
388 if err == io.EOF {
389 return nil
390 } else if err != nil {
391 return err
392 }
393
394 if b == '\n' {
395 return nil
396 }
397 }
398}
399
400func (zp *zncParser) sectionHeader() (string, string, error) {
401 if err := zp.expectRune('<'); err != nil {
402 return "", "", err
403 }
404 typ, err := zp.readWord(' ')
405 if err != nil {
406 return "", "", err
407 }
408 name, err := zp.readWord('>')
409 return typ, name, err
410}
411
412func (zp *zncParser) sectionFooter() (string, error) {
413 if err := zp.expectRune('<'); err != nil {
414 return "", err
415 }
416 if err := zp.expectRune('/'); err != nil {
417 return "", err
418 }
419 return zp.readWord('>')
420}
421
422func (zp *zncParser) keyValuePair() (string, string, error) {
423 k, err := zp.readWord('=')
424 if err != nil {
425 return "", "", err
426 }
427 v, err := zp.readWord('\n')
428 return strings.TrimSpace(k), strings.TrimSpace(v), err
429}
430
431func (zp *zncParser) expectRune(expected rune) error {
432 r, _, err := zp.readRune()
433 if err != nil {
434 return err
435 } else if r != expected {
436 return fmt.Errorf("expected %q, got %q", expected, r)
437 }
438 return nil
439}
440
441func (zp *zncParser) readWord(delim byte) (string, error) {
442 var sb strings.Builder
443 for {
444 b, err := zp.readByte()
445 if err != nil {
446 return "", err
447 }
448
449 if b == delim {
450 return sb.String(), nil
451 }
452 if b == '\n' {
453 return "", fmt.Errorf("expected %q before newline", delim)
454 }
455
456 sb.WriteByte(b)
457 }
458}
459
460type stringSetFlag map[string]bool
461
462func (v *stringSetFlag) String() string {
463 return fmt.Sprint(map[string]bool(*v))
464}
465
466func (v *stringSetFlag) Set(s string) error {
467 (*v)[s] = true
468 return nil
469}
Note: See TracBrowser for help on using the repository browser.