workaround for goexmpp bug
[svn42.git] / go / r3-netstatus / r3xmppbot / r3xmppbot.go
1 // (c) Bernhard Tittelbach, 2013
2
3 package r3xmppbot
4
5 import (
6         "crypto/tls"
7         "encoding/json"
8         "errors"
9         "os"
10         "path"
11         "strings"
12         "time"
13
14         xmpp "code.google.com/p/goexmpp"
15 )
16
17 func (botdata *XmppBot) makeXMPPMessage(to string, message interface{}, subject interface{}) *xmpp.Message {
18         xmppmsgheader := xmpp.Header{To: to,
19                 From:     botdata.my_jid_,
20                 Id:       <-xmpp.Id,
21                 Type:     "chat",
22                 Lang:     "",
23                 Innerxml: "",
24                 Error:    nil,
25                 Nested:   make([]interface{}, 0)}
26
27         var msgsubject, msgbody *xmpp.Generic
28         switch cast_msg := message.(type) {
29         case string:
30                 msgbody = &xmpp.Generic{Chardata: cast_msg}
31         case *string:
32                 msgbody = &xmpp.Generic{Chardata: *cast_msg}
33         case *xmpp.Generic:
34                 msgbody = cast_msg
35         default:
36                 msgbody = &xmpp.Generic{}
37         }
38         switch cast_msg := subject.(type) {
39         case string:
40                 msgsubject = &xmpp.Generic{Chardata: cast_msg}
41         case *string:
42                 msgsubject = &xmpp.Generic{Chardata: *cast_msg}
43         case *xmpp.Generic:
44                 msgsubject = cast_msg
45         default:
46                 msgsubject = &xmpp.Generic{}
47         }
48         return &xmpp.Message{Header: xmppmsgheader, Subject: msgsubject, Body: msgbody, Thread: &xmpp.Generic{}}
49 }
50
51 func (botdata *XmppBot) makeXMPPPresence(to, ptype, show, status string) *xmpp.Presence {
52         xmppmsgheader := xmpp.Header{To: to,
53                 From:     botdata.my_jid_,
54                 Id:       <-xmpp.Id,
55                 Type:     ptype,
56                 Lang:     "",
57                 Innerxml: "",
58                 Error:    nil,
59                 Nested:   make([]interface{}, 0)}
60         var gen_show, gen_status *xmpp.Generic
61         if len(show) == 0 {
62                 gen_show = nil
63         } else {
64                 gen_show = &xmpp.Generic{Chardata: show}
65         }
66         if len(status) == 0 {
67                 gen_status = nil
68         } else {
69                 gen_status = &xmpp.Generic{Chardata: status}
70         }
71         return &xmpp.Presence{Header: xmppmsgheader, Show: gen_show, Status: gen_status}
72 }
73
74 type R3JIDDesire int
75
76 const (
77         R3NoChange  R3JIDDesire = -1
78         R3NeverInfo R3JIDDesire = iota // ignore first value by assigning to blank identifier
79         R3OnlineOnlyInfo
80         R3OnlineOnlyWithRecapInfo
81         R3AlwaysInfo
82         R3DebugInfo
83 )
84
85 const (
86         ShowOnline       string = ""
87         ShowAway         string = "away"
88         ShowNotAvailabe  string = "xa"
89         ShowDoNotDisturb string = "dnd"
90         ShowFreeForChat  string = "chat"
91 )
92
93 type JidData struct {
94         Online bool
95         Wants  R3JIDDesire
96 }
97
98 type JabberEvent struct {
99         JID       string
100         Online    bool
101         Wants     R3JIDDesire
102         StatusNow bool
103 }
104
105 type XMPPMsgEvent struct {
106         Msg              string
107         DistributeLevel  R3JIDDesire
108         RememberAsStatus bool
109 }
110
111 type XMPPStatusEvent struct {
112         Show   string
113         Status string
114 }
115
116 type RealraumXmppNotifierConfig map[string]JidData
117
118 type XmppBot struct {
119         jid_lastauthtime_  map[string]int64
120         realraum_jids_     RealraumXmppNotifierConfig
121         password_          string
122         my_jid_            string
123         auth_timeout_      int64
124         config_file_       string
125         my_login_password_ string
126         xmppclient_        *xmpp.Client
127         presence_events_   *chan interface{}
128 }
129
130 func (data RealraumXmppNotifierConfig) saveTo(filepath string) {
131         fh, err := os.Create(filepath)
132         if err != nil {
133                 Syslog_.Println(err)
134                 return
135         }
136         defer fh.Close()
137         enc := json.NewEncoder(fh)
138         if err = enc.Encode(&data); err != nil {
139                 Syslog_.Println(err)
140                 return
141         }
142 }
143
144 func (data RealraumXmppNotifierConfig) loadFrom(filepath string) {
145         fh, err := os.Open(filepath)
146         if err != nil {
147                 Syslog_.Println(err)
148                 return
149         }
150         defer fh.Close()
151         dec := json.NewDecoder(fh)
152         if err = dec.Decode(&data); err != nil {
153                 Syslog_.Println(err)
154                 return
155         }
156         for to, jiddata := range data {
157                 jiddata.Online = false
158                 data[to] = jiddata
159         }
160 }
161
162 func (botdata *XmppBot) handleEventsforXMPP(xmppout chan<- xmpp.Stanza, presence_events <-chan interface{}, jabber_events <-chan JabberEvent) {
163         var last_status_msg *string
164
165         defer func() {
166                 if x := recover(); x != nil {
167                         Syslog_.Printf("handleEventsforXMPP: run time panic: %v", x)
168                 }
169                 for _ = range jabber_events {
170                 } //cleanout jabber_events queue
171         }()
172
173         for {
174                 select {
175                 case pe, pe_still_open := <-presence_events:
176                         if !pe_still_open {
177                                 return
178                         }
179                         Debug_.Printf("handleEventsforXMPP<-presence_events: %T %+v", pe, pe)
180                         switch pec := pe.(type) {
181                         case xmpp.Stanza:
182                                 xmppout <- pec
183                                 continue
184                         case string:
185                                 for to, jiddata := range botdata.realraum_jids_ {
186                                         if jiddata.Wants >= R3DebugInfo {
187                                                 xmppout <- botdata.makeXMPPMessage(to, pec, nil)
188                                         }
189                                 }
190
191                         case XMPPStatusEvent:
192                                 xmppout <- botdata.makeXMPPPresence("", "", pec.Show, pec.Status)
193
194                         case XMPPMsgEvent:
195                                 if pec.RememberAsStatus {
196                                         last_status_msg = &pec.Msg
197                                 }
198                                 for to, jiddata := range botdata.realraum_jids_ {
199                                         if jiddata.Wants >= pec.DistributeLevel && ((jiddata.Wants >= R3OnlineOnlyInfo && jiddata.Online) || jiddata.Wants >= R3AlwaysInfo) {
200                                                 xmppout <- botdata.makeXMPPMessage(to, pec.Msg, nil)
201                                         }
202                                 }
203                         default:
204                                 Debug_.Println("handleEventsforXMPP<-presence_events: unknown type received: quitting")
205                                 return
206                         }
207
208                 case je, je_still_open := <-jabber_events:
209                         if !je_still_open {
210                                 return
211                         }
212                         Debug_.Printf("handleEventsforXMPP<-jabber_events: %T %+v", je, je)
213                         simple_jid := removeJIDResource(je.JID)
214                         jid_data, jid_in_map := botdata.realraum_jids_[simple_jid]
215
216                         //send status if requested, even if user never changed any settings and thus is not in map
217                         if last_status_msg != nil && je.StatusNow {
218                                 xmppout <- botdata.makeXMPPMessage(je.JID, last_status_msg, nil)
219                         }
220
221                         if jid_in_map {
222                                 //if R3OnlineOnlyWithRecapInfo, we want a status update when coming online
223                                 if last_status_msg != nil && !jid_data.Online && je.Online && jid_data.Wants == R3OnlineOnlyWithRecapInfo {
224                                         xmppout <- botdata.makeXMPPMessage(je.JID, last_status_msg, nil)
225                                 }
226                                 jid_data.Online = je.Online
227                                 if je.Wants > R3NoChange {
228                                         jid_data.Wants = je.Wants
229                                 }
230                                 botdata.realraum_jids_[simple_jid] = jid_data
231                                 botdata.realraum_jids_.saveTo(botdata.config_file_)
232                         } else if je.Wants > R3NoChange {
233                                 botdata.realraum_jids_[simple_jid] = JidData{je.Online, je.Wants}
234                                 botdata.realraum_jids_.saveTo(botdata.config_file_)
235                         }
236                 }
237         }
238 }
239
240 func removeJIDResource(jid string) string {
241         var jidjid xmpp.JID
242         jidjid.Set(jid)
243         jidjid.Resource = ""
244         return jidjid.String()
245 }
246
247 func (botdata *XmppBot) isAuthenticated(jid string) bool {
248         authtime, in_map := botdata.jid_lastauthtime_[jid]
249         return in_map && time.Now().Unix()-authtime < botdata.auth_timeout_
250 }
251
252 const help_text_ string = "\n*auth*<password>* ...Enables you to use more commands.\n*time* ...Returns bot time."
253 const help_text_auth string = "You are authorized to use the following commands:\n*off* ...You will no longer receive notifications.\n*on* ...You will be notified of r3 status changes while you are online.\n*on_with_recap* ...Like *on* but additionally you will receive the current status when you come online.\n*on_while_offline* ...You will receive all r3 status changes, wether you are online or offline.\n*status* ...Use it to query the current status.\n*time* ...Returns bot time.\n*bye* ...Logout."
254
255 //~ var re_msg_auth_    *regexp.Regexp     = regexp.MustCompile("auth\s+(\S+)")
256
257 func (botdata *XmppBot) handleIncomingMessageDialog(inmsg xmpp.Message, xmppout chan<- xmpp.Stanza, jabber_events chan JabberEvent) {
258         if inmsg.Body == nil || inmsg.GetHeader() == nil {
259                 return
260         }
261         bodytext_args := strings.Split(strings.Replace(inmsg.Body.Chardata, "*", " ", -1), " ")
262         for len(bodytext_args) > 1 && len(bodytext_args[0]) == 0 {
263                 bodytext_args = bodytext_args[1:len(bodytext_args)] //get rid of empty first strings resulting from " text"
264         }
265         bodytext_lc_cmd := strings.ToLower(bodytext_args[0])
266         if botdata.isAuthenticated(inmsg.GetHeader().From) {
267                 switch bodytext_lc_cmd {
268                 case "on":
269                         jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3OnlineOnlyInfo, false}
270                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Receive r3 status updates while online.", "Your New Status")
271                 case "off":
272                         jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3NeverInfo, false}
273                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Do not receive anything.", "Your New Status")
274                 case "on_with_recap":
275                         jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3OnlineOnlyWithRecapInfo, false}
276                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Receive r3 status updates while and current status on coming, online.", "Your New Status")
277                 case "on_while_offline":
278                         jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3AlwaysInfo, false}
279                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Receive all r3 status updates, even if you are offline.", "Your New Status")
280                 case "debug":
281                         jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3DebugInfo, false}
282                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Debug mode enabled", "Your New Status")
283                 case "bye", "quit", "logout":
284                         botdata.jid_lastauthtime_[inmsg.GetHeader().From] = 0
285                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Bye Bye !", nil)
286                 case "open", "close":
287                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Sorry, I'm just weak software, not strong enough to operate the door for you.", nil)
288                 case "status":
289                         jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3NoChange, true}
290                 case "time":
291                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, time.Now().String(), nil)
292                 case "ping":
293                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Pong with auth", nil)
294                 default:
295                         //~ auth_match = re_msg_auth_.FindStringSubmatch(inmsg.Body.Chardata)
296                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, help_text_auth, nil)
297                 }
298         } else {
299                 switch bodytext_lc_cmd {
300                 case "hilfe", "help", "?", "hallo", "yes", "ja", "ja bitte", "bitte", "sowieso":
301                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, help_text_, "Available Commands")
302                 case "auth":
303                         authindex := 1
304                         for len(bodytext_args) > authindex && len(bodytext_args[authindex]) == 0 {
305                                 authindex++
306                         }
307                         if len(bodytext_args) > authindex && bodytext_args[authindex] == botdata.password_ {
308                                 botdata.jid_lastauthtime_[inmsg.GetHeader().From] = time.Now().Unix()
309                                 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, help_text_auth, nil)
310                         }
311                 case "status", "off", "on", "on_while_offline", "on_with_recap":
312                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Sorry, you need to be authorized to do that.", nil)
313                 case "time":
314                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, time.Now().String(), nil)
315                 case "ping":
316                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Pong", nil)
317                 case "":
318                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "You're a quiet one, aren't you?", nil)
319                 default:
320                         //~ auth_match = re_msg_auth_.FindStringSubmatch(inmsg.Body.Chardata)
321                         xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "A nice day to you too !\nDo you need \"help\" ?", nil)
322                 }
323         }
324 }
325
326 func (botdata *XmppBot) handleIncomingXMPPStanzas(xmppin <-chan xmpp.Stanza, xmppout chan<- xmpp.Stanza, jabber_events chan JabberEvent) {
327
328         defer func() {
329                 if x := recover(); x != nil {
330                         Syslog_.Printf("handleIncomingXMPPStanzas: run time panic: %v", x)
331                 }
332         }()
333
334         var error_count int = 0
335         var incoming_stanza interface{}
336
337         handleStanzaError := func() bool {
338                 error_count++
339                 if error_count > 15 {
340                         Syslog_.Println("handleIncomingXMPPStanzas: too many errors in series.. bailing out")
341                         botdata.StopBot()
342                         return true
343                 }
344                 return false
345         }
346
347         for incoming_stanza = range xmppin {
348                 switch stanza := incoming_stanza.(type) {
349                 case *xmpp.Message:
350                         if stanza.GetHeader() == nil {
351                                 continue
352                         }
353                         if stanza.Type == "error" || stanza.Error != nil {
354                                 Syslog_.Printf("XMPP %T Error: %s", stanza, stanza)
355                                 if stanza.Error.Type == "cancel" {
356                                         // asume receipient not reachable -> disable
357                                         Syslog_.Printf("Error reaching %s. Disabling user, please reenable manually", stanza.From)
358                                         jabber_events <- JabberEvent{stanza.From, false, R3NeverInfo, false}
359                                         continue
360                                 }
361                                 if handleStanzaError() {
362                                         return
363                                 }
364                                 continue
365                         } else {
366                                 error_count = 0
367                         }
368                         botdata.handleIncomingMessageDialog(*stanza, xmppout, jabber_events)
369                 case *xmpp.Presence:
370                         if stanza.GetHeader() == nil {
371                                 continue
372                         }
373                         if stanza.Type == "error" || stanza.Error != nil {
374                                 Syslog_.Printf("XMPP %T Error: %s", stanza, stanza)
375                                 if handleStanzaError() {
376                                         return
377                                 }
378                                 continue
379                         } else {
380                                 error_count = 0
381                         }
382                         switch stanza.GetHeader().Type {
383                         case "subscribe":
384                                 xmppout <- botdata.makeXMPPPresence(stanza.GetHeader().From, "subscribed", "", "")
385                                 jabber_events <- JabberEvent{stanza.GetHeader().From, true, R3NoChange, false}
386                                 xmppout <- botdata.makeXMPPPresence(stanza.GetHeader().From, "subscribe", "", "")
387                         case "unsubscribe", "unsubscribed":
388                                 jabber_events <- JabberEvent{stanza.GetHeader().From, false, R3NeverInfo, false}
389                                 botdata.jid_lastauthtime_[stanza.GetHeader().From] = 0 //logout
390                                 xmppout <- botdata.makeXMPPPresence(stanza.GetHeader().From, "unsubscribe", "", "")
391                         case "unavailable":
392                                 jabber_events <- JabberEvent{stanza.GetHeader().From, false, R3NoChange, false}
393                                 botdata.jid_lastauthtime_[stanza.GetHeader().From] = 0 //logout
394                         default:
395                                 jabber_events <- JabberEvent{stanza.GetHeader().From, true, R3NoChange, false}
396                         }
397
398                 case *xmpp.Iq:
399                         if stanza.GetHeader() == nil {
400                                 continue
401                         }
402                         if stanza.Type == "error" || stanza.Error != nil {
403                                 Syslog_.Printf("XMPP %T Error: %s", stanza, stanza)
404                                 if handleStanzaError() {
405                                         return
406                                 }
407                                 continue
408                         } else {
409                                 error_count = 0
410                         }
411
412                         if HandleServerToClientPing(stanza, xmppout) {
413                                 continue
414                         } //if true then routine handled it and we can continue
415                         Debug_.Printf("Unhandled Iq: %s", stanza)
416                 }
417         }
418 }
419
420 func init() {
421         //~ xmpp.Debug = &XMPPDebugLogger{}
422         xmpp.Info = &XMPPDebugLogger{}
423         xmpp.Warn = &XMPPLogger{}
424 }
425
426 func NewStartedBot(loginjid, loginpwd, password, state_save_dir string, insecuretls bool) (*XmppBot, chan interface{}, error) {
427         var err error
428         botdata := new(XmppBot)
429
430         botdata.realraum_jids_ = make(map[string]JidData, 1)
431         botdata.jid_lastauthtime_ = make(map[string]int64, 1)
432         botdata.my_jid_ = loginjid
433         botdata.my_login_password_ = loginpwd
434         botdata.password_ = password
435         botdata.auth_timeout_ = 3600 * 2
436
437         botdata.config_file_ = path.Join(state_save_dir, "r3xmpp."+removeJIDResource(loginjid)+".json")
438
439         xmpp.TlsConfig = tls.Config{InsecureSkipVerify: insecuretls}
440         botdata.realraum_jids_.loadFrom(botdata.config_file_)
441
442         client_jid := new(xmpp.JID)
443         client_jid.Set(botdata.my_jid_)
444         botdata.xmppclient_, err = xmpp.NewClient(client_jid, botdata.my_login_password_, nil)
445         if err != nil {
446                 Syslog_.Println("Error connecting to xmpp server", err)
447                 return nil, nil, err
448         }
449         if botdata.xmppclient_ == nil {
450                 Syslog_.Println("xmpp.NewClient returned nil without error")
451                 return nil, nil, errors.New("No answer from xmpp server")
452         }
453
454         err = botdata.xmppclient_.StartSession(true, &xmpp.Presence{})
455         if err != nil {
456                 Syslog_.Println("'Error StartSession:", err)
457                 return nil, nil, err
458         }
459
460         roster := xmpp.Roster(botdata.xmppclient_)
461         for _, entry := range roster {
462                 Debug_.Print(entry)
463                 if entry.Subscription == "from" {
464                         botdata.xmppclient_.Out <- botdata.makeXMPPPresence(entry.Jid, "subscribe", "", "")
465                 }
466                 if entry.Subscription == "none" {
467                         delete(botdata.realraum_jids_, entry.Jid)
468                 }
469         }
470
471         presence_events := make(chan interface{}, 1)
472         jabber_events := make(chan JabberEvent, 1)
473
474         go func() {
475                 for { //auto recover from panic
476                         botdata.handleEventsforXMPP(botdata.xmppclient_.Out, presence_events, jabber_events)
477                 }
478         }()
479         go func() {
480                 for { //auto recover from panic
481                         botdata.handleIncomingXMPPStanzas(botdata.xmppclient_.In, botdata.xmppclient_.Out, jabber_events)
482                 }
483         }()
484
485         botdata.presence_events_ = &presence_events
486
487         return botdata, presence_events, nil
488 }
489
490 func (botdata *XmppBot) StopBot() {
491         Syslog_.Println("Stopping XMPP Bot")
492         if botdata.xmppclient_ != nil {
493                 close(botdata.xmppclient_.Out)
494         }
495         if botdata.presence_events_ != nil {
496                 *botdata.presence_events_ <- false
497                 close(*botdata.presence_events_)
498         }
499         botdata.config_file_ = ""
500         botdata.realraum_jids_ = nil
501         botdata.xmppclient_ = nil
502 }