package gateway import ( "bytes" "encoding/base64" "encoding/xml" "github.com/pkg/errors" "fmt" "io" "sort" "strings" "sync" "dev.narayana.im/narayana/telegabber/xmpp/extensions" log "github.com/sirupsen/logrus" "github.com/soheilhy/args" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) type Reply struct { Author string Id string Start uint64 End uint64 } const NSNick string = "http://jabber.org/protocol/nick" // Queue stores presences to send later var Queue = make(map[string]*stanza.Presence) var QueueLock = sync.Mutex{} // Jid stores the component's JID object var Jid *stanza.Jid // DirtySessions denotes that some Telegram session configurations // were changed and need to be re-flushed to the YamlDB var DirtySessions = false // MessageOutgoingPermission allows to fake outgoing messages by foreign JIDs var MessageOutgoingPermission = false // CapsType is a capability category type CapsType int const ( CapsAudio CapsType = iota ) // ContactType is a disco JID category type ContactType int const ( ContactTransport CapsType = iota ContactPM ) // SendMessage creates and sends a message stanza func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isOutgoing bool) { sendMessageWrapper(to, from, body, id, component, reply, "", isOutgoing) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to string, body string, component *xmpp.Component) { sendMessageWrapper(to, "", body, "", component, nil, "", false) } // SendTextMessage creates and sends a simple message stanza func SendTextMessage(to string, from string, body string, component *xmpp.Component) { sendMessageWrapper(to, from, body, "", component, nil, "", false) } // SendMessageWithOOB creates and sends a message stanza with OOB URL func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isOutgoing bool) { sendMessageWrapper(to, from, body, id, component, reply, oob, isOutgoing) } func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isOutgoing bool) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ "to": to, }).Error(errors.Wrap(err, "Invalid to JID!")) return } bareTo := toJid.Bare() componentJid := Jid.Full() var logFrom string var messageFrom string var messageTo string if from == "" { logFrom = componentJid messageFrom = componentJid } else { logFrom = from messageFrom = from + "@" + componentJid } if isOutgoing { messageTo = messageFrom messageFrom = bareTo + "/" + Jid.Resource } else { messageTo = to } log.WithFields(log.Fields{ "from": logFrom, "to": to, }).Warn("Got message") message := stanza.Message{ Attrs: stanza.Attrs{ From: messageFrom, To: messageTo, Type: "chat", Id: id, }, Body: body, } if oob != "" { message.Extensions = append(message.Extensions, stanza.OOB{ URL: oob, }) } if reply != nil { message.Extensions = append(message.Extensions, extensions.Reply{ To: reply.Author, Id: reply.Id, }) if reply.End > 0 { message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) } } if isOutgoing { carbonMessage := extensions.ClientMessage{ Attrs: stanza.Attrs{ From: bareTo, To: to, Type: "chat", }, } carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{ Forwarded: stanza.Forwarded{ Stanza: extensions.ClientMessage(message), }, }) privilegeMessage := stanza.Message{ Attrs: stanza.Attrs{ From: Jid.Bare(), To: toJid.Domain, }, } privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege{ Forwarded: stanza.Forwarded{ Stanza: carbonMessage, }, }) sendMessage(&privilegeMessage, component) } else { sendMessage(&message, component) } } // SetNickname sets a new nickname for a contact func SetNickname(to string, from string, nickname string, component *xmpp.Component) { componentJid := Jid.Bare() messageFrom := from + "@" + componentJid log.WithFields(log.Fields{ "from": from, "to": to, }).Warn("Set nickname") message := stanza.Message{ Attrs: stanza.Attrs{ From: messageFrom, To: to, Type: "headline", }, Extensions: []stanza.MsgExtension{ stanza.PubSubEvent{ EventElement: stanza.ItemsEvent{ Node: NSNick, Items: []stanza.ItemEvent{ stanza.ItemEvent{ Any: &stanza.Node{ XMLName: xml.Name{Space: NSNick, Local: "nick"}, Content: nickname, }, }, }, }, }, }, } sendMessage(&message, component) } func sendMessage(message *stanza.Message, component *xmpp.Component) { // explicit check, as marshalling is expensive if log.GetLevel() == log.DebugLevel { xmlMessage, err := xml.Marshal(message) if err == nil { log.Debug(string(xmlMessage)) } else { log.Debugf("%#v", message) } } _ = ResumableSend(component, message) } // LogBadPresence verbosely logs a presence func LogBadPresence(presence *stanza.Presence) { log.Errorf("Couldn't send presence: %#v", presence) } // SPFrom is a Telegram user id var SPFrom = args.NewString() // SPType is a presence type var SPType = args.NewString() // SPShow is a availability status var SPShow = args.NewString() // SPStatus is a verbose status var SPStatus = args.NewString() // SPNickname is a XEP-0172 nickname var SPNickname = args.NewString() // SPPhoto is a XEP-0153 hash of avatar in vCard var SPPhoto = args.NewString() // SPResource is an optional resource var SPResource = args.NewString() // SPImmed skips queueing var SPImmed = args.NewBool(args.Default(true)) // SPCaps is a XEP-0115 verification string var SPCaps = args.NewString() func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { var presenceFrom string if SPFrom.IsSet(args) { presenceFrom = SPFrom.Get(args) + "@" + bareJid if SPResource.IsSet(args) { resource := SPResource.Get(args) if resource != "" { presenceFrom += "/" + resource } } } else { presenceFrom = bareJid } presence := stanza.Presence{Attrs: stanza.Attrs{ From: presenceFrom, To: to, }} if SPType.IsSet(args) { t := SPType.Get(args) if t != "" { presence.Attrs.Type = stanza.StanzaType(t) } } if SPShow.IsSet(args) { show := SPShow.Get(args) if show != "" { presence.Show = stanza.PresenceShow(show) } } if SPStatus.IsSet(args) { status := SPStatus.Get(args) if status != "" { presence.Status = status } } if SPNickname.IsSet(args) { nickname := SPNickname.Get(args) if nickname != "" { presence.Extensions = append(presence.Extensions, extensions.PresenceNickExtension{ Text: nickname, }) } } if SPPhoto.IsSet(args) { photo := SPPhoto.Get(args) if photo != "" { presence.Extensions = append(presence.Extensions, extensions.PresenceXVCardUpdateExtension{ Photo: extensions.PresenceXVCardUpdatePhoto{ Text: photo, }, }) } } if SPCaps.IsSet(args) { ver := SPCaps.Get(args) if ver != "" { presence.Extensions = append(presence.Extensions, extensions.CapsExtension{ Hash: "sha-1", Node: "https://dev.narayana.im/narayana/telegabber/", Ver: ver, }) } } return presence } // SendPresence creates and sends a presence stanza func SendPresence(component *xmpp.Component, to string, args ...args.V) error { var logFrom string bareJid := Jid.Bare() if SPFrom.IsSet(args) { logFrom = SPFrom.Get(args) } else { logFrom = bareJid } log.WithFields(log.Fields{ "type": SPType.Get(args), "from": logFrom, "to": to, }).Info("Got presence") presence := newPresence(bareJid, to, args...) // explicit check, as marshalling is expensive if log.GetLevel() == log.DebugLevel { xmlPresence, err := xml.Marshal(presence) if err == nil { log.Debug(string(xmlPresence)) } else { log.Debugf("%#v", presence) } } immed := SPImmed.Get(args) if immed { err := ResumableSend(component, presence) if err != nil { LogBadPresence(&presence) return err } } else { QueueLock.Lock() Queue[presence.From+presence.To] = &presence QueueLock.Unlock() } return nil } // ResumableSend tries to resume the connection once and sends the packet again func ResumableSend(component *xmpp.Component, packet stanza.Packet) error { err := component.Send(packet) if err != nil && strings.HasPrefix(err.Error(), "cannot send packet") { log.Warn("Packet send failed, trying to resume the connection...") err = component.Connect() if err == nil { err = component.Send(packet) } } if err != nil { log.Error(err.Error()) } return err } // SplitJID tokenizes a JID string to bare JID and resource func SplitJID(from string) (string, string, bool) { fromJid, err := stanza.NewJid(from) if err != nil { log.WithFields(log.Fields{ "from": from, }).Error(errors.Wrap(err, "Invalid from JID!")) return "", "", false } return fromJid.Bare(), fromJid.Resource, true } func getDiscoFeatures(caps []CapsType) []string { features := []string{ "http://jabber.org/protocol/caps", "http://jabber.org/protocol/disco#info", } for typ := range features { switch typ { case CapsAudio: features = append( features, "urn:xmpp:jingle-message:0", "urn:xmpp:jingle:1", "urn:xmpp:jingle:apps:dtls:0", "urn:xmpp:jingle:apps:rtp:1", "urn:xmpp:jingle:apps:rtp:audio", "urn:xmpp:jingle:transports:ice-udp:1", ) } } return features } // GetDiscoInfo generates a disco info IQ query response func GetDiscoInfo(typ ContactType, features []string) *stanza.DiscoInfo { disco := stanza.DiscoInfo{} if typ == ContactPM { disco.AddIdentity("", "account", "registered") } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") } disco.AddFeatures(features...) return &disco } // GetCapsVer hashes a capabilities set into a verification string func GetCapsVer(caps []CapsType) (string, error) { features := getDiscoFeatures(caps) disco := GetDiscoInfo(features) discoToCapsHash(disco) buf := new(bytes.Buffer) binval := base64.NewEncoder(base64.StdEncoding, buf) _, err = io.Copy(binval, file) binval.Close() if err != nil { return "", errors.Wrap(err, "Error calculating caps base64") } return buf.String(), nil } func iOctetComparator(a, b string) bool { return a < b } func discoToCaps(disco *stanza.DiscoInfo) string { var s strings.Builder var identities, vars, capsForms []string for _, identity := range disco.Identity { identities = append(identities, fmt.Sprintf( "%s/%s//%s", identity.Category, identity.Type, identity.Name, )) } sort.Slice(identities, iOctetComparator) for _, identity := range identities { s.WriteString(identity) s.WriteString(">") } for _, feature := range disco.Features { vars = append(vars, feature.Var) } sort.Slice(vars, iOctetComparator) for _, var := range vars { s.WriteString(var) s.WriteString(">") } for disco s.WriteString( }