diff --git a/go.mod b/go.mod index 03dcbe1..d600614 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module code.hackerspace.pl/ar/notbot go 1.16 -require gopkg.in/irc.v3 v3.1.4 +require ( + golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d + gopkg.in/irc.v3 v3.1.4 +) diff --git a/go.sum b/go.sum index 12af36d..4ee1cb0 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,13 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/irc.v3 v3.1.4 h1:DYGMRFbtseXEh+NadmMUFzMraqyuUj4I3iWYFEzDZPc= gopkg.in/irc.v3 v3.1.4/go.mod h1:shO2gz8+PVeS+4E6GAny88Z0YVVQSxQghdrMVGQsR9s= diff --git a/gomod2nix.toml b/gomod2nix.toml index 8af4f08..c5efcd3 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -30,6 +30,46 @@ rev = "221dbe5ed46703ee255b1da0dec05086f5035f62" sha256 = "187i5g88sxfy4vxpm7dw1gwv29pa2qaq475lxrdh5livh69wqfjb" +["golang.org/x/net"] + sumVersion = "v0.0.0-20220114011407-0dd24b26b47d" + ["golang.org/x/net".fetch] + type = "git" + url = "https://go.googlesource.com/net" + rev = "0dd24b26b47d4eb2d45eb3c7a4bcb809d7c1edb8" + sha256 = "0k8s5y6nkw7q1g3rdgnv33q96jrxi19ni2p587wy9z48jcgk6w2i" + +["golang.org/x/sys"] + sumVersion = "v0.0.0-20210423082822-04245dca01da" + ["golang.org/x/sys".fetch] + type = "git" + url = "https://go.googlesource.com/sys" + rev = "04245dca01dae530ad36275d662a90d6b8a5ee29" + sha256 = "11is2c5cpxk0gf2mxza2wpzfcf71fxb9b3in77f6w2q0pr68ykzx" + +["golang.org/x/term"] + sumVersion = "v0.0.0-20201126162022-7de9c90e9dd1" + ["golang.org/x/term".fetch] + type = "git" + url = "https://go.googlesource.com/term" + rev = "7de9c90e9dd184706b838f536a1cbf40a296ddb7" + sha256 = "1ba252xmv6qsvf1w1gcy98mngrj0vd4inbjw0lsklqvva65nljna" + +["golang.org/x/text"] + sumVersion = "v0.3.6" + ["golang.org/x/text".fetch] + type = "git" + url = "https://go.googlesource.com/text" + rev = "e328d63cff14134669501e0e154e4f141c784322" + sha256 = "0wzhvdb059vrp2cczqw422ajrb9sbs4l3qd020hlngj33qfhxah0" + +["golang.org/x/tools"] + sumVersion = "v0.0.0-20180917221912-90fa682c2a6e" + ["golang.org/x/tools".fetch] + type = "git" + url = "https://go.googlesource.com/tools" + rev = "90fa682c2a6e6a37b3a1364ce2fe1d5e41af9d6d" + sha256 = "03ic2xsy51jw9749wl7gszdbz99iijbd2bckgygl6cm9w5m364ak" + ["gopkg.in/check.v1"] sumVersion = "v0.0.0-20161208181325-20d25e280405" ["gopkg.in/check.v1".fetch] diff --git a/jitsi.go b/jitsi.go new file mode 100644 index 0000000..ae8323d --- /dev/null +++ b/jitsi.go @@ -0,0 +1,220 @@ +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) 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: + _, 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 != "" { // 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 + 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\n", j.ircChannel, nickZws) + 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) + nickZws := knownNick[:1] + "\u200B" + knownNick[1:] + ircMsg := fmt.Sprintf("NOTICE %s :jitsi: -%s\n", j.ircChannel, nickZws) + 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) +}