Skip to content
Snippets Groups Projects
jitsimeetbridge.py 10.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env python
    
    """
    This is an attempt at bridging matrix clients into a Jitis meet room via Matrix
    video call.  It uses hard-coded xml strings overg XMPP BOSH. It can display one
    of the streams from the Jitsi bridge until the second lot of SDP comes down and
    we set the remote SDP at which point the stream ends. Our video never gets to
    the bridge.
    
    Requires:
    
    npm install jquery jsdom
    
    """
    import json
    import subprocess
    import time
    
    
    import gevent
    import grequests
    from BeautifulSoup import BeautifulSoup
    
    ACCESS_TOKEN = ""
    
    Amber Brown's avatar
    Amber Brown committed
    MATRIXBASE = "https://matrix.org/_matrix/client/api/v1/"
    MYUSERNAME = "@davetest:matrix.org"
    
    Amber Brown's avatar
    Amber Brown committed
    HTTPBIND = "https://meet.jit.si/http-bind"
    # HTTPBIND = 'https://jitsi.vuc.me/http-bind'
    # ROOMNAME = "matrix"
    
    ROOMNAME = "pibble"
    
    
    Amber Brown's avatar
    Amber Brown committed
    HOST = "guest.jit.si"
    # HOST="jitsi.vuc.me"
    
    Amber Brown's avatar
    Amber Brown committed
    TURNSERVER = "turn.guest.jit.si"
    # TURNSERVER="turn.jitsi.vuc.me"
    
    ROOMDOMAIN = "meet.jit.si"
    # ROOMDOMAIN="conference.jitsi.vuc.me"
    
    
    
    class TrivialMatrixClient:
    
        def __init__(self, access_token):
            self.token = None
            self.access_token = access_token
    
        def getEvent(self):
            while True:
    
    Amber Brown's avatar
    Amber Brown committed
                url = (
                    MATRIXBASE
                    + "events?access_token="
                    + self.access_token
                    + "&timeout=60000"
                )
    
                if self.token:
    
    Amber Brown's avatar
    Amber Brown committed
                    url += "&from=" + self.token
    
                req = grequests.get(url)
                resps = grequests.map([req])
                obj = json.loads(resps[0].content)
    
    Amber Brown's avatar
    Amber Brown committed
                print("incoming from matrix", obj)
                if "end" not in obj:
    
    Amber Brown's avatar
    Amber Brown committed
                self.token = obj["end"]
                if len(obj["chunk"]):
                    return obj["chunk"][0]
    
    
        def joinRoom(self, roomId):
    
    Amber Brown's avatar
    Amber Brown committed
            url = MATRIXBASE + "rooms/" + roomId + "/join?access_token=" + self.access_token
    
    Amber Brown's avatar
    Amber Brown committed
            headers = {"Content-Type": "application/json"}
            req = grequests.post(url, headers=headers, data="{}")
    
            resps = grequests.map([req])
            obj = json.loads(resps[0].content)
    
    Amber Brown's avatar
    Amber Brown committed
            print("response: ", obj)
    
    
        def sendEvent(self, roomId, evType, event):
    
    Amber Brown's avatar
    Amber Brown committed
            url = (
                MATRIXBASE
                + "rooms/"
                + roomId
                + "/send/"
                + evType
                + "?access_token="
                + self.access_token
            )
    
            print(url)
            print(json.dumps(event))
    
    Amber Brown's avatar
    Amber Brown committed
            headers = {"Content-Type": "application/json"}
    
            req = grequests.post(url, headers=headers, data=json.dumps(event))
            resps = grequests.map([req])
            obj = json.loads(resps[0].content)
    
    Amber Brown's avatar
    Amber Brown committed
            print("response: ", obj)
    
    
    
    xmppClients = {}
    
    
    def matrixLoop():
    
        while True:
            ev = matrixCli.getEvent()
    
    Amber Brown's avatar
    Amber Brown committed
            if ev["type"] == "m.room.member":
                print("membership event")
                if ev["membership"] == "invite" and ev["state_key"] == MYUSERNAME:
                    roomId = ev["room_id"]
    
                    print("joining room %s" % (roomId))
    
                    matrixCli.joinRoom(roomId)
    
    Amber Brown's avatar
    Amber Brown committed
            elif ev["type"] == "m.room.message":
                if ev["room_id"] in xmppClients:
    
                    print("already have a bridge for that user, ignoring")
    
                print("got message, connecting")
    
    Amber Brown's avatar
    Amber Brown committed
                xmppClients[ev["room_id"]] = TrivialXmppClient(ev["room_id"], ev["user_id"])
                gevent.spawn(xmppClients[ev["room_id"]].xmppLoop)
            elif ev["type"] == "m.call.invite":
    
                print("Incoming call")
    
    Amber Brown's avatar
    Amber Brown committed
                # sdp = ev['content']['offer']['sdp']
                # print "sdp: %s" % (sdp)
                # xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
                # gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
            elif ev["type"] == "m.call.answer":
    
                print("Call answered")
    
    Amber Brown's avatar
    Amber Brown committed
                sdp = ev["content"]["answer"]["sdp"]
                if ev["room_id"] not in xmppClients:
    
                    print("We didn't have a call for that room")
    
                    continue
                # should probably check call ID too
    
    Amber Brown's avatar
    Amber Brown committed
                xmppCli = xmppClients[ev["room_id"]]
    
                xmppCli.sendAnswer(sdp)
    
    Amber Brown's avatar
    Amber Brown committed
            elif ev["type"] == "m.call.hangup":
                if ev["room_id"] in xmppClients:
                    xmppClients[ev["room_id"]].stop()
                    del xmppClients[ev["room_id"]]
    
    
    class TrivialXmppClient:
    
        def __init__(self, matrixRoom, userId):
            self.rid = 0
            self.matrixRoom = matrixRoom
            self.userId = userId
            self.running = True
    
        def stop(self):
            self.running = False
    
        def nextRid(self):
            self.rid += 1
    
    Amber Brown's avatar
    Amber Brown committed
            return "%d" % (self.rid)
    
    
        def sendIq(self, xml):
    
    Amber Brown's avatar
    Amber Brown committed
            fullXml = (
                "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>"
                % (self.nextRid(), self.sid, xml)
            )
            # print "\t>>>%s" % (fullXml)
    
            return self.xmppPoke(fullXml)
    
        def xmppPoke(self, xml):
    
    Amber Brown's avatar
    Amber Brown committed
            headers = {"Content-Type": "application/xml"}
    
            req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
            resps = grequests.map([req])
            obj = BeautifulSoup(resps[0].content)
            return obj
    
        def sendAnswer(self, answer):
    
    Amber Brown's avatar
    Amber Brown committed
            print("sdp from matrix client", answer)
            p = subprocess.Popen(
                ["node", "unjingle/unjingle.js", "--sdp"],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
            )
    
            jingle, out_err = p.communicate(answer)
            jingle = jingle % {
    
    Amber Brown's avatar
    Amber Brown committed
                "tojid": self.callfrom,
                "action": "session-accept",
                "initiator": self.callfrom,
                "responder": self.jid,
                "sid": self.callsid,
    
    Amber Brown's avatar
    Amber Brown committed
            print("answer jingle from sdp", jingle)
    
            res = self.sendIq(jingle)
    
    Amber Brown's avatar
    Amber Brown committed
            print("reply from answer: ", res)
    
    
            self.ssrcs = {}
            jingleSoup = BeautifulSoup(jingle)
    
    Amber Brown's avatar
    Amber Brown committed
            for cont in jingleSoup.iq.jingle.findAll("content"):
    
                if cont.description:
    
    Amber Brown's avatar
    Amber Brown committed
                    self.ssrcs[cont["name"]] = cont.description["ssrc"]
            print("my ssrcs:", self.ssrcs)
    
    Amber Brown's avatar
    Amber Brown committed
            gevent.joinall([gevent.spawn(self.advertiseSsrcs)])
    
    
        def advertiseSsrcs(self):
    
            time.sleep(7)
            print("SSRC spammer started")
    
            while self.running:
    
                ssrcMsg = "<presence to='%(tojid)s' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%(nick)s</nick><stats xmlns='http://jitsi.org/jitmeet/stats'><stat name='bitrate_download' value='175'/><stat name='bitrate_upload' value='176'/><stat name='packetLoss_total' value='0'/><stat name='packetLoss_download' value='0'/><stat name='packetLoss_upload' value='0'/></stats><media xmlns='http://estos.de/ns/mjs'><source type='audio' ssrc='%(assrc)s' direction='sendre'/><source type='video' ssrc='%(vssrc)s' direction='sendre'/></media></presence>" % {
                    "tojid": "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid),
                    "nick": self.userId,
                    "assrc": self.ssrcs["audio"],
                    "vssrc": self.ssrcs["video"],
                }
    
                res = self.sendIq(ssrcMsg)
    
    Amber Brown's avatar
    Amber Brown committed
                print("reply from ssrc announce: ", res)
    
                time.sleep(10)
    
        def xmppLoop(self):
            self.matrixCallId = time.time()
    
    Amber Brown's avatar
    Amber Brown committed
            res = self.xmppPoke(
                "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' to='%s' xml:lang='en' wait='60' hold='1' content='text/xml; charset=utf-8' ver='1.6' xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'/>"
                % (self.nextRid(), HOST)
            )
    
    Amber Brown's avatar
    Amber Brown committed
            self.sid = res.body["sid"]
    
            print("sid %s" % (self.sid))
    
    Amber Brown's avatar
    Amber Brown committed
            res = self.sendIq(
                "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>"
            )
    
    Amber Brown's avatar
    Amber Brown committed
            res = self.xmppPoke(
                "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s' to='%s' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>"
                % (self.nextRid(), self.sid, HOST)
            )
    
    Amber Brown's avatar
    Amber Brown committed
            res = self.sendIq(
                "<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>"
            )
    
    
            self.jid = res.body.iq.bind.jid.string
    
            print("jid: %s" % (self.jid))
    
    Amber Brown's avatar
    Amber Brown committed
            self.shortJid = self.jid.split("-")[0]
    
    Amber Brown's avatar
    Amber Brown committed
            res = self.sendIq(
                "<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>"
            )
    
    Amber Brown's avatar
    Amber Brown committed
            # randomthing = res.body.iq['to']
            # whatsitpart = randomthing.split('-')[0]
    
    Amber Brown's avatar
    Amber Brown committed
            # print "other random bind thing: %s" % (randomthing)
    
    
            # advertise preence to the jitsi room, with our nick
    
    Amber Brown's avatar
    Amber Brown committed
            res = self.sendIq(
                "<iq type='get' to='%s' xmlns='jabber:client' id='1:sendIQ'><services xmlns='urn:xmpp:extdisco:1'><service host='%s'/></services></iq><presence to='%s@%s/d98f6c40' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%s</nick></presence>"
                % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId)
            )
            self.muc = {"users": []}
            for p in res.body.findAll("presence"):
    
    Amber Brown's avatar
    Amber Brown committed
                u["shortJid"] = p["from"].split("/")[1]
    
                if p.c and p.c.nick:
    
    Amber Brown's avatar
    Amber Brown committed
                    u["nick"] = p.c.nick.string
                self.muc["users"].append(u)
            print("muc: ", self.muc)
    
    
            # wait for stuff
            while True:
    
                res = self.sendIq("")
    
    Amber Brown's avatar
    Amber Brown committed
                print("got from stream: ", res)
    
                if res.body.iq:
    
    Amber Brown's avatar
    Amber Brown committed
                    jingles = res.body.iq.findAll("jingle")
    
                    if len(jingles):
    
    Amber Brown's avatar
    Amber Brown committed
                        self.callfrom = res.body.iq["from"]
    
                        self.handleInvite(jingles[0])
    
    Amber Brown's avatar
    Amber Brown committed
                elif "type" in res.body and res.body["type"] == "terminate":
    
                    self.running = False
                    del xmppClients[self.matrixRoom]
    
    
        def handleInvite(self, jingle):
    
    Amber Brown's avatar
    Amber Brown committed
            self.initiator = jingle["initiator"]
            self.callsid = jingle["sid"]
            p = subprocess.Popen(
                ["node", "unjingle/unjingle.js", "--jingle"],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
            )
            print("raw jingle invite", str(jingle))
    
            sdp, out_err = p.communicate(str(jingle))
    
    Amber Brown's avatar
    Amber Brown committed
            print("transformed remote offer sdp", sdp)
    
            inviteEvent = {
    
    Amber Brown's avatar
    Amber Brown committed
                "offer": {"type": "offer", "sdp": sdp},
                "call_id": self.matrixCallId,
                "version": 0,
                "lifetime": 30000,
    
    Amber Brown's avatar
    Amber Brown committed
            matrixCli.sendEvent(self.matrixRoom, "m.call.invite", inviteEvent)
    
    Amber Brown's avatar
    Amber Brown committed
    matrixCli = TrivialMatrixClient(ACCESS_TOKEN)  # Undefined name
    
    Amber Brown's avatar
    Amber Brown committed
    gevent.joinall([gevent.spawn(matrixLoop)])