1 // (c) Bernhard Tittelbach, 2013
14 xmpp "code.google.com/p/goexmpp"
17 func (botdata *XmppBot) makeXMPPMessage(to string, message interface{}, subject interface{}) *xmpp.Message {
18 xmppmsgheader := xmpp.Header{To: to,
19 From: botdata.my_jid_,
25 Nested: make([]interface{}, 0)}
27 var msgsubject, msgbody *xmpp.Generic
28 switch cast_msg := message.(type) {
30 msgbody = &xmpp.Generic{Chardata: cast_msg}
32 msgbody = &xmpp.Generic{Chardata: *cast_msg}
36 msgbody = &xmpp.Generic{}
38 switch cast_msg := subject.(type) {
40 msgsubject = &xmpp.Generic{Chardata: cast_msg}
42 msgsubject = &xmpp.Generic{Chardata: *cast_msg}
46 msgsubject = &xmpp.Generic{}
48 return &xmpp.Message{Header: xmppmsgheader, Subject: msgsubject, Body: msgbody, Thread: &xmpp.Generic{}}
51 func (botdata *XmppBot) makeXMPPPresence(to, ptype, show, status string) *xmpp.Presence {
52 xmppmsgheader := xmpp.Header{To: to,
53 From: botdata.my_jid_,
59 Nested: make([]interface{}, 0)}
60 var gen_show, gen_status *xmpp.Generic
64 gen_show = &xmpp.Generic{Chardata: show}
69 gen_status = &xmpp.Generic{Chardata: status}
71 return &xmpp.Presence{Header: xmppmsgheader, Show: gen_show, Status: gen_status}
77 R3NoChange R3JIDDesire = -1
78 R3NeverInfo R3JIDDesire = iota // ignore first value by assigning to blank identifier
80 R3OnlineOnlyWithRecapInfo
86 ShowOnline string = ""
87 ShowAway string = "away"
88 ShowNotAvailabe string = "xa"
89 ShowDoNotDisturb string = "dnd"
90 ShowFreeForChat string = "chat"
98 type JabberEvent struct {
105 type XMPPMsgEvent struct {
107 DistributeLevel R3JIDDesire
108 RememberAsStatus bool
111 type XMPPStatusEvent struct {
116 type RealraumXmppNotifierConfig map[string]JidData
118 type XmppBot struct {
119 jid_lastauthtime_ map[string]int64
120 realraum_jids_ RealraumXmppNotifierConfig
125 my_login_password_ string
126 xmppclient_ *xmpp.Client
127 presence_events_ *chan interface{}
130 func (data RealraumXmppNotifierConfig) saveTo(filepath string) {
131 fh, err := os.Create(filepath)
137 enc := json.NewEncoder(fh)
138 if err = enc.Encode(&data); err != nil {
144 func (data RealraumXmppNotifierConfig) loadFrom(filepath string) {
145 fh, err := os.Open(filepath)
151 dec := json.NewDecoder(fh)
152 if err = dec.Decode(&data); err != nil {
156 for to, jiddata := range data {
157 jiddata.Online = false
162 func (botdata *XmppBot) handleEventsforXMPP(xmppout chan<- xmpp.Stanza, presence_events <-chan interface{}, jabber_events <-chan JabberEvent) {
163 var last_status_msg *string
166 if x := recover(); x != nil {
167 Syslog_.Printf("handleEventsforXMPP: run time panic: %v", x)
169 for _ = range jabber_events {
170 } //cleanout jabber_events queue
175 case pe, pe_still_open := <-presence_events:
179 Debug_.Printf("handleEventsforXMPP<-presence_events: %T %+v", pe, pe)
180 switch pec := pe.(type) {
185 for to, jiddata := range botdata.realraum_jids_ {
186 if jiddata.Wants >= R3DebugInfo {
187 xmppout <- botdata.makeXMPPMessage(to, pec, nil)
191 case XMPPStatusEvent:
192 xmppout <- botdata.makeXMPPPresence("", "", pec.Show, pec.Status)
195 if pec.RememberAsStatus {
196 last_status_msg = &pec.Msg
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)
204 Debug_.Println("handleEventsforXMPP<-presence_events: unknown type received: quitting")
208 case je, je_still_open := <-jabber_events:
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]
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)
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)
226 jid_data.Online = je.Online
227 if je.Wants > R3NoChange {
228 jid_data.Wants = je.Wants
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_)
240 func removeJIDResource(jid string) string {
244 return jidjid.String()
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_
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."
255 //~ var re_msg_auth_ *regexp.Regexp = regexp.MustCompile("auth\s+(\S+)")
257 func (botdata *XmppBot) handleIncomingMessageDialog(inmsg xmpp.Message, xmppout chan<- xmpp.Stanza, jabber_events chan JabberEvent) {
258 if inmsg.Body == nil || inmsg.GetHeader() == nil {
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"
265 bodytext_lc_cmd := strings.ToLower(bodytext_args[0])
266 if botdata.isAuthenticated(inmsg.GetHeader().From) {
267 switch bodytext_lc_cmd {
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")
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")
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)
289 jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3NoChange, true}
291 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, time.Now().String(), nil)
293 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Pong with auth", nil)
295 //~ auth_match = re_msg_auth_.FindStringSubmatch(inmsg.Body.Chardata)
296 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, help_text_auth, nil)
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")
304 for len(bodytext_args) > authindex && len(bodytext_args[authindex]) == 0 {
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)
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)
314 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, time.Now().String(), nil)
316 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Pong", nil)
318 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "You're a quiet one, aren't you?", nil)
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)
326 func (botdata *XmppBot) handleIncomingXMPPStanzas(xmppin <-chan xmpp.Stanza, xmppout chan<- xmpp.Stanza, jabber_events chan JabberEvent) {
329 if x := recover(); x != nil {
330 Syslog_.Printf("handleIncomingXMPPStanzas: run time panic: %v", x)
334 var error_count int = 0
335 var incoming_stanza interface{}
337 handleStanzaError := func() bool {
339 if error_count > 15 {
340 Syslog_.Println("handleIncomingXMPPStanzas: too many errors in series.. bailing out")
347 for incoming_stanza = range xmppin {
348 switch stanza := incoming_stanza.(type) {
350 if stanza.GetHeader() == nil {
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}
361 if handleStanzaError() {
368 botdata.handleIncomingMessageDialog(*stanza, xmppout, jabber_events)
370 if stanza.GetHeader() == nil {
373 if stanza.Type == "error" || stanza.Error != nil {
374 Syslog_.Printf("XMPP %T Error: %s", stanza, stanza)
375 if handleStanzaError() {
382 switch stanza.GetHeader().Type {
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", "", "")
392 jabber_events <- JabberEvent{stanza.GetHeader().From, false, R3NoChange, false}
393 botdata.jid_lastauthtime_[stanza.GetHeader().From] = 0 //logout
395 jabber_events <- JabberEvent{stanza.GetHeader().From, true, R3NoChange, false}
399 if stanza.GetHeader() == nil {
402 if stanza.Type == "error" || stanza.Error != nil {
403 Syslog_.Printf("XMPP %T Error: %s", stanza, stanza)
404 if handleStanzaError() {
412 if HandleServerToClientPing(stanza, xmppout) {
414 } //if true then routine handled it and we can continue
415 Debug_.Printf("Unhandled Iq: %s", stanza)
421 //~ xmpp.Debug = &XMPPDebugLogger{}
422 xmpp.Info = &XMPPDebugLogger{}
423 xmpp.Warn = &XMPPLogger{}
426 func NewStartedBot(loginjid, loginpwd, password, state_save_dir string, insecuretls bool) (*XmppBot, chan interface{}, error) {
428 botdata := new(XmppBot)
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
437 botdata.config_file_ = path.Join(state_save_dir, "r3xmpp."+removeJIDResource(loginjid)+".json")
439 xmpp.TlsConfig = tls.Config{InsecureSkipVerify: insecuretls}
440 botdata.realraum_jids_.loadFrom(botdata.config_file_)
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)
446 Syslog_.Println("Error connecting to xmpp server", err)
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")
454 err = botdata.xmppclient_.StartSession(true, &xmpp.Presence{})
456 Syslog_.Println("'Error StartSession:", err)
460 roster := xmpp.Roster(botdata.xmppclient_)
461 for _, entry := range roster {
463 if entry.Subscription == "from" {
464 botdata.xmppclient_.Out <- botdata.makeXMPPPresence(entry.Jid, "subscribe", "", "")
466 if entry.Subscription == "none" {
467 delete(botdata.realraum_jids_, entry.Jid)
471 presence_events := make(chan interface{}, 1)
472 jabber_events := make(chan JabberEvent, 1)
475 for { //auto recover from panic
476 botdata.handleEventsforXMPP(botdata.xmppclient_.Out, presence_events, jabber_events)
480 for { //auto recover from panic
481 botdata.handleIncomingXMPPStanzas(botdata.xmppclient_.In, botdata.xmppclient_.Out, jabber_events)
485 botdata.presence_events_ = &presence_events
487 return botdata, presence_events, nil
490 func (botdata *XmppBot) StopBot() {
491 Syslog_.Println("Stopping XMPP Bot")
492 if botdata.xmppclient_ != nil {
493 close(botdata.xmppclient_.Out)
495 if botdata.presence_events_ != nil {
496 *botdata.presence_events_ <- false
497 close(*botdata.presence_events_)
499 botdata.config_file_ = ""
500 botdata.realraum_jids_ = nil
501 botdata.xmppclient_ = nil