package main import ( "encoding/xml" "flag" "fmt" "log" "strings" "time" "golang.org/x/net/websocket" "gopkg.in/irc.v3" ) const pingFrame = "" var ( jitsiChannels arrayFlags ) // we actually only care about .Type, .Nick, and .X.Item.Jid type JitsiPresence struct { XMLName xml.Name `xml:"presence"` Text string `xml:",chardata"` From string `xml:"from,attr"` To string `xml:"to,attr"` Type string `xml:"type,attr"` Xmlns string `xml:"xmlns,attr"` StatsID string `xml:"stats-id"` Region struct { Text string `xml:",chardata"` Xmlns string `xml:"xmlns,attr"` ID string `xml:"id,attr"` } `xml:"region"` C struct { Text string `xml:",chardata"` Ver string `xml:"ver,attr"` Hash string `xml:"hash,attr"` Xmlns string `xml:"xmlns,attr"` Node string `xml:"node,attr"` } `xml:"c"` Features struct { Text string `xml:",chardata"` Feature struct { Text string `xml:",chardata"` Var string `xml:"var,attr"` } `xml:"feature"` } `xml:"features"` JitsiParticipantRegion string `xml:"jitsi_participant_region"` JitsiParticipantCodecType string `xml:"jitsi_participant_codecType"` Nick struct { Text string `xml:",chardata"` Xmlns string `xml:"xmlns,attr"` } `xml:"nick"` JitsiParticipantE2eeIdKey string `xml:"jitsi_participant_e2ee.idKey"` Audiomuted string `xml:"audiomuted"` Videomuted string `xml:"videomuted"` X struct { Text string `xml:",chardata"` Xmlns string `xml:"xmlns,attr"` Photo string `xml:"photo"` Item struct { Text string `xml:",chardata"` Affiliation string `xml:"affiliation,attr"` Role string `xml:"role,attr"` Jid string `xml:"jid,attr"` } `xml:"item"` } `xml:"x"` } type JitsiClient struct { nick string server string room string ircChannel string done chan bool users map[string]string // map[jid]nick } func (j *JitsiClient) UserListZWS() (ret []string) { for _, user := range j.users { u := user[:1] + "\u200B" + user[1:] ret = append(ret, u) } return ret } func (j *JitsiClient) KeepAlive(ws *websocket.Conn) { ticker := time.NewTicker(5 * time.Second) for { select { case <-ticker.C: if _, err := ws.Write([]byte(pingFrame)); err != nil { log.Println("JitsiClient", j.server, j.room, "Error while sending ping", err) return } } } } func (j *JitsiClient) Run(c *irc.Client, done chan bool) { var msg = make([]byte, 64*1024) origin := "https://" + j.server url := "wss://" + j.server + "/xmpp-websocket?room=" + j.room protocol := "xmpp" var initFrames = []string{ "", "", "", "", "", "" + "Joy-4gA" + "" + "ffmuc-de1truetrue" + "" + j.nick + "", } j.users = make(map[string]string) for { log.Println("JitsiClient", j.server, j.room, "Initializing") ws, err := websocket.Dial(url, protocol, origin) if err != nil { log.Println("JitsiClient", j.server, j.room, "Error connecting to websocket", url, err) goto reconnect } for n, frame := range initFrames { if _, err := ws.Write([]byte(frame)); err != nil { log.Println("JitsiClient", j.server, j.room, "Error sending initialization frame", n, err) goto reconnect } } log.Println("JitsiClient", j.server, j.room, "Running") go j.KeepAlive(ws) for { select { case <-done: log.Println("JitsiClient", j.server, j.room, "Shutting down") return default: var alsoThere string _, err := ws.Read(msg) v := JitsiPresence{} if err != nil { log.Println("JitsiClient", j.server, j.room, "Error while reading from websocket", err) goto reconnect } err = xml.Unmarshal(msg, &v) if err != nil { // xml parsing errors will be normal here continue } if v.Nick.Text == j.nick { continue } if v.Nick.Text != "" { // if presence event has Nick present, it *shouldn't* mean that user has left the chat if v.X.Item.Jid != "" { if knownNick, ok := j.users[v.X.Item.Jid]; ok { if knownNick != v.Nick.Text { // user changed nickname, we don't care about that enough log.Println("JitsiClient", j.server, j.room, "User changed nickname:", knownNick, v.Nick.Text) j.users[v.X.Item.Jid] = v.Nick.Text continue } } else { // new user previousList := j.UserListZWS() if len(previousList) > 0 { alsoThere = fmt.Sprint(", also there:", previousList) } j.users[v.X.Item.Jid] = v.Nick.Text nickZws := v.Nick.Text[:1] + "\u200B" + v.Nick.Text[1:] ircMsg := fmt.Sprintf("NOTICE %s :jitsi: +%s%s\n", j.ircChannel, nickZws, alsoThere) log.Println("JitsiClient", j.server, j.room, "User joined:", j.users[v.X.Item.Jid]) c.Write(ircMsg) continue } } } if v.Type == "unavailable" { if v.X.Item.Jid != "" { if knownNick, ok := j.users[v.X.Item.Jid]; ok { delete(j.users, v.X.Item.Jid) userList := j.UserListZWS() if len(userList) > 0 { alsoThere = fmt.Sprint(", still there:", userList) } nickZws := knownNick[:1] + "\u200B" + knownNick[1:] ircMsg := fmt.Sprintf("NOTICE %s :jitsi: -%s%s\n", j.ircChannel, nickZws, alsoThere) log.Println("JitsiClient", j.server, j.room, "User left:", knownNick) c.Write(ircMsg) continue } } } } } reconnect: time.Sleep(1 * time.Second) log.Println("JitsiClient", j.server, j.room, "Reconnecting...") } } func JitsiRunWrapper(c *irc.Client, done chan bool) { jitsiDone := make([]chan bool, len(jitsiChannels)) for i, ch := range jitsiChannels { args := strings.Split(ch, ",") if len(args) != 3 { log.Fatalln("Wrong jitsi channel mapping format", ch, args) } j := JitsiClient{ nick: nickname, ircChannel: args[0], server: args[1], room: args[2], } go j.Run(c, jitsiDone[i]) } <-done for _, ch := range jitsiDone { ch <- true } } func init() { flag.Var(&jitsiChannels, "jitsi.channels", "ircChannel,jitsiServer,jitsiRoom mapping; may be specified multiple times") Runners.Add(JitsiRunWrapper) }