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