1 // (c) Bernhard Tittelbach, 2013
13 xmpp "code.google.com/p/goexmpp"
16 func (botdata *XmppBot) makeXMPPMessage(to string, message interface{}, subject interface{}) *xmpp.Message {
17 xmppmsgheader := xmpp.Header{To: to,
18 From: botdata.my_jid_,
24 Nested: make([]interface{}, 0)}
26 var msgsubject, msgbody *xmpp.Generic
27 switch cast_msg := message.(type) {
29 msgbody = &xmpp.Generic{Chardata: cast_msg}
31 msgbody = &xmpp.Generic{Chardata: *cast_msg}
35 msgbody = &xmpp.Generic{}
37 switch cast_msg := subject.(type) {
39 msgsubject = &xmpp.Generic{Chardata: cast_msg}
41 msgsubject = &xmpp.Generic{Chardata: *cast_msg}
45 msgsubject = &xmpp.Generic{}
47 return &xmpp.Message{Header: xmppmsgheader, Subject: msgsubject, Body: msgbody, Thread: &xmpp.Generic{}}
50 func (botdata *XmppBot) makeXMPPPresence(to, ptype, show, status string) *xmpp.Presence {
51 xmppmsgheader := xmpp.Header{To: to,
52 From: botdata.my_jid_,
58 Nested: make([]interface{}, 0)}
59 var gen_show, gen_status *xmpp.Generic
63 gen_show = &xmpp.Generic{Chardata: show}
68 gen_status = &xmpp.Generic{Chardata: status}
70 return &xmpp.Presence{Header: xmppmsgheader, Show: gen_show, Status: gen_status}
76 R3NoChange R3JIDDesire = -1
77 R3NeverInfo R3JIDDesire = iota // ignore first value by assigning to blank identifier
79 R3OnlineOnlyWithRecapInfo
85 ShowOnline string = ""
86 ShowAway string = "away"
87 ShowNotAvailabe string = "xa"
88 ShowDoNotDisturb string = "dnd"
89 ShowFreeForChat string = "chat"
97 type JabberEvent struct {
104 type XMPPMsgEvent struct {
106 DistributeLevel R3JIDDesire
107 RememberAsStatus bool
110 type XMPPStatusEvent struct {
115 type RealraumXmppNotifierConfig map[string]JidData
117 type XmppBot struct {
118 jid_lastauthtime_ map[string]int64
119 realraum_jids_ RealraumXmppNotifierConfig
124 my_login_password_ string
125 xmppclient_ *xmpp.Client
126 presence_events_ *chan interface{}
129 func (data RealraumXmppNotifierConfig) saveTo(filepath string) {
130 fh, err := os.Create(filepath)
136 enc := json.NewEncoder(fh)
137 if err = enc.Encode(&data); err != nil {
143 func (data RealraumXmppNotifierConfig) loadFrom(filepath string) {
144 fh, err := os.Open(filepath)
150 dec := json.NewDecoder(fh)
151 if err = dec.Decode(&data); err != nil {
155 for to, jiddata := range data {
156 jiddata.Online = false
161 func (botdata *XmppBot) handleEventsforXMPP(xmppout chan<- xmpp.Stanza, presence_events <-chan interface{}, jabber_events <-chan JabberEvent) {
162 var last_status_msg *string
165 if x := recover(); x != nil {
166 Syslog_.Printf("handleEventsforXMPP: run time panic: %v", x)
168 for _ = range jabber_events {
169 } //cleanout jabber_events queue
174 case pe, pe_still_open := <-presence_events:
178 Debug_.Printf("handleEventsforXMPP<-presence_events: %T %+v", pe, pe)
179 switch pec := pe.(type) {
184 for to, jiddata := range botdata.realraum_jids_ {
185 if jiddata.Wants >= R3DebugInfo {
186 xmppout <- botdata.makeXMPPMessage(to, pec, nil)
190 case XMPPStatusEvent:
191 xmppout <- botdata.makeXMPPPresence("", "", pec.Show, pec.Status)
194 if pec.RememberAsStatus {
195 last_status_msg = &pec.Msg
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)
203 Debug_.Println("handleEventsforXMPP<-presence_events: unknown type received: quitting")
207 case je, je_still_open := <-jabber_events:
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]
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)
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)
225 jid_data.Online = je.Online
226 if je.Wants > R3NoChange {
227 jid_data.Wants = je.Wants
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_)
239 func removeJIDResource(jid string) string {
243 return jidjid.String()
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_
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."
254 //~ var re_msg_auth_ *regexp.Regexp = regexp.MustCompile("auth\s+(\S+)")
256 func (botdata *XmppBot) handleIncomingMessageDialog(inmsg xmpp.Message, xmppout chan<- xmpp.Stanza, jabber_events chan JabberEvent) {
257 if inmsg.Body == nil || inmsg.GetHeader() == nil {
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"
264 bodytext_lc_cmd := strings.ToLower(bodytext_args[0])
265 if botdata.isAuthenticated(inmsg.GetHeader().From) {
266 switch bodytext_lc_cmd {
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")
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")
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)
288 jabber_events <- JabberEvent{inmsg.GetHeader().From, true, R3NoChange, true}
290 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, time.Now().String(), nil)
292 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Pong with auth", nil)
294 //~ auth_match = re_msg_auth_.FindStringSubmatch(inmsg.Body.Chardata)
295 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, help_text_auth, nil)
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")
303 for len(bodytext_args) > authindex && len(bodytext_args[authindex]) == 0 {
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)
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)
313 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, time.Now().String(), nil)
315 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "Pong", nil)
317 xmppout <- botdata.makeXMPPMessage(inmsg.GetHeader().From, "You're a quiet one, aren't you?", nil)
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)
325 func (botdata *XmppBot) handleIncomingXMPPStanzas(xmppin <-chan xmpp.Stanza, xmppout chan<- xmpp.Stanza, jabber_events chan JabberEvent) {
328 if x := recover(); x != nil {
329 Syslog_.Printf("handleIncomingXMPPStanzas: run time panic: %v", x)
333 var error_count int = 0
334 var incoming_stanza interface{}
336 handleStanzaError := func() bool {
338 if error_count > 15 {
339 Syslog_.Println("handleIncomingXMPPStanzas: too many errors in series.. bailing out")
346 for incoming_stanza = range xmppin {
347 switch stanza := incoming_stanza.(type) {
349 if stanza.GetHeader() == nil {
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}
360 if handleStanzaError() {
367 botdata.handleIncomingMessageDialog(*stanza, xmppout, jabber_events)
369 if stanza.GetHeader() == nil {
372 if stanza.Type == "error" || stanza.Error != nil {
373 Syslog_.Printf("XMPP %T Error: %s", stanza, stanza)
374 if handleStanzaError() {
381 switch stanza.GetHeader().Type {
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", "", "")
391 jabber_events <- JabberEvent{stanza.GetHeader().From, false, R3NoChange, false}
392 botdata.jid_lastauthtime_[stanza.GetHeader().From] = 0 //logout
394 jabber_events <- JabberEvent{stanza.GetHeader().From, true, R3NoChange, false}
398 if stanza.GetHeader() == nil {
401 if stanza.Type == "error" || stanza.Error != nil {
402 Syslog_.Printf("XMPP %T Error: %s", stanza, stanza)
403 if handleStanzaError() {
411 if HandleServerToClientPing(stanza, xmppout) {
413 } //if true then routine handled it and we can continue
414 Debug_.Printf("Unhandled Iq: %s", stanza)
420 //~ xmpp.Debug = &XMPPDebugLogger{}
421 xmpp.Info = &XMPPDebugLogger{}
422 xmpp.Warn = &XMPPLogger{}
425 func NewStartedBot(loginjid, loginpwd, password, state_save_dir string, insecuretls bool) (*XmppBot, chan interface{}, error) {
427 botdata := new(XmppBot)
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
436 botdata.config_file_ = path.Join(state_save_dir, "r3xmpp."+removeJIDResource(loginjid)+".json")
438 xmpp.TlsConfig = tls.Config{InsecureSkipVerify: insecuretls}
439 botdata.realraum_jids_.loadFrom(botdata.config_file_)
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)
445 Syslog_.Println("Error connecting to xmpp server", err)
449 err = botdata.xmppclient_.StartSession(true, &xmpp.Presence{})
451 Syslog_.Println("'Error StartSession:", err)
455 roster := xmpp.Roster(botdata.xmppclient_)
456 for _, entry := range roster {
458 if entry.Subscription == "from" {
459 botdata.xmppclient_.Out <- botdata.makeXMPPPresence(entry.Jid, "subscribe", "", "")
461 if entry.Subscription == "none" {
462 delete(botdata.realraum_jids_, entry.Jid)
466 presence_events := make(chan interface{}, 1)
467 jabber_events := make(chan JabberEvent, 1)
470 for { //auto recover from panic
471 botdata.handleEventsforXMPP(botdata.xmppclient_.Out, presence_events, jabber_events)
475 for { //auto recover from panic
476 botdata.handleIncomingXMPPStanzas(botdata.xmppclient_.In, botdata.xmppclient_.Out, jabber_events)
480 botdata.presence_events_ = &presence_events
482 return botdata, presence_events, nil
485 func (botdata *XmppBot) StopBot() {
486 Syslog_.Println("Stopping XMPP Bot")
487 if botdata.xmppclient_ != nil {
488 close(botdata.xmppclient_.Out)
490 if botdata.presence_events_ != nil {
491 *botdata.presence_events_ <- false
492 close(*botdata.presence_events_)
494 botdata.config_file_ = ""
495 botdata.realraum_jids_ = nil
496 botdata.xmppclient_ = nil