301 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			301 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
| #!/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
 | |
| """
 | |
| from __future__ import print_function
 | |
| 
 | |
| import gevent
 | |
| import grequests
 | |
| from BeautifulSoup import BeautifulSoup
 | |
| import json
 | |
| import urllib
 | |
| import subprocess
 | |
| import time
 | |
| 
 | |
| # ACCESS_TOKEN="" #
 | |
| 
 | |
| MATRIXBASE = "https://matrix.org/_matrix/client/api/v1/"
 | |
| MYUSERNAME = "@davetest:matrix.org"
 | |
| 
 | |
| HTTPBIND = "https://meet.jit.si/http-bind"
 | |
| # HTTPBIND = 'https://jitsi.vuc.me/http-bind'
 | |
| # ROOMNAME = "matrix"
 | |
| ROOMNAME = "pibble"
 | |
| 
 | |
| HOST = "guest.jit.si"
 | |
| # HOST="jitsi.vuc.me"
 | |
| 
 | |
| 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:
 | |
|             url = (
 | |
|                 MATRIXBASE
 | |
|                 + "events?access_token="
 | |
|                 + self.access_token
 | |
|                 + "&timeout=60000"
 | |
|             )
 | |
|             if self.token:
 | |
|                 url += "&from=" + self.token
 | |
|             req = grequests.get(url)
 | |
|             resps = grequests.map([req])
 | |
|             obj = json.loads(resps[0].content)
 | |
|             print("incoming from matrix", obj)
 | |
|             if "end" not in obj:
 | |
|                 continue
 | |
|             self.token = obj["end"]
 | |
|             if len(obj["chunk"]):
 | |
|                 return obj["chunk"][0]
 | |
| 
 | |
|     def joinRoom(self, roomId):
 | |
|         url = MATRIXBASE + "rooms/" + roomId + "/join?access_token=" + self.access_token
 | |
|         print(url)
 | |
|         headers = {"Content-Type": "application/json"}
 | |
|         req = grequests.post(url, headers=headers, data="{}")
 | |
|         resps = grequests.map([req])
 | |
|         obj = json.loads(resps[0].content)
 | |
|         print("response: ", obj)
 | |
| 
 | |
|     def sendEvent(self, roomId, evType, event):
 | |
|         url = (
 | |
|             MATRIXBASE
 | |
|             + "rooms/"
 | |
|             + roomId
 | |
|             + "/send/"
 | |
|             + evType
 | |
|             + "?access_token="
 | |
|             + self.access_token
 | |
|         )
 | |
|         print(url)
 | |
|         print(json.dumps(event))
 | |
|         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)
 | |
|         print("response: ", obj)
 | |
| 
 | |
| 
 | |
| xmppClients = {}
 | |
| 
 | |
| 
 | |
| def matrixLoop():
 | |
|     while True:
 | |
|         ev = matrixCli.getEvent()
 | |
|         print(ev)
 | |
|         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)
 | |
|         elif ev["type"] == "m.room.message":
 | |
|             if ev["room_id"] in xmppClients:
 | |
|                 print("already have a bridge for that user, ignoring")
 | |
|                 continue
 | |
|             print("got message, connecting")
 | |
|             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")
 | |
|             # 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")
 | |
|             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
 | |
|             xmppCli = xmppClients[ev["room_id"]]
 | |
|             xmppCli.sendAnswer(sdp)
 | |
|         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
 | |
|         return "%d" % (self.rid)
 | |
| 
 | |
|     def sendIq(self, xml):
 | |
|         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):
 | |
|         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):
 | |
|         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 % {
 | |
|             "tojid": self.callfrom,
 | |
|             "action": "session-accept",
 | |
|             "initiator": self.callfrom,
 | |
|             "responder": self.jid,
 | |
|             "sid": self.callsid,
 | |
|         }
 | |
|         print("answer jingle from sdp", jingle)
 | |
|         res = self.sendIq(jingle)
 | |
|         print("reply from answer: ", res)
 | |
| 
 | |
|         self.ssrcs = {}
 | |
|         jingleSoup = BeautifulSoup(jingle)
 | |
|         for cont in jingleSoup.iq.jingle.findAll("content"):
 | |
|             if cont.description:
 | |
|                 self.ssrcs[cont["name"]] = cont.description["ssrc"]
 | |
|         print("my ssrcs:", self.ssrcs)
 | |
| 
 | |
|         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)
 | |
|             print("reply from ssrc announce: ", res)
 | |
|             time.sleep(10)
 | |
| 
 | |
|     def xmppLoop(self):
 | |
|         self.matrixCallId = time.time()
 | |
|         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)
 | |
|         )
 | |
| 
 | |
|         print(res)
 | |
|         self.sid = res.body["sid"]
 | |
|         print("sid %s" % (self.sid))
 | |
| 
 | |
|         res = self.sendIq(
 | |
|             "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>"
 | |
|         )
 | |
| 
 | |
|         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)
 | |
|         )
 | |
| 
 | |
|         res = self.sendIq(
 | |
|             "<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>"
 | |
|         )
 | |
|         print(res)
 | |
| 
 | |
|         self.jid = res.body.iq.bind.jid.string
 | |
|         print("jid: %s" % (self.jid))
 | |
|         self.shortJid = self.jid.split("-")[0]
 | |
| 
 | |
|         res = self.sendIq(
 | |
|             "<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>"
 | |
|         )
 | |
| 
 | |
|         # randomthing = res.body.iq['to']
 | |
|         # whatsitpart = randomthing.split('-')[0]
 | |
| 
 | |
|         # print "other random bind thing: %s" % (randomthing)
 | |
| 
 | |
|         # advertise preence to the jitsi room, with our nick
 | |
|         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"):
 | |
|             u = {}
 | |
|             u["shortJid"] = p["from"].split("/")[1]
 | |
|             if p.c and p.c.nick:
 | |
|                 u["nick"] = p.c.nick.string
 | |
|             self.muc["users"].append(u)
 | |
|         print("muc: ", self.muc)
 | |
| 
 | |
|         # wait for stuff
 | |
|         while True:
 | |
|             print("waiting...")
 | |
|             res = self.sendIq("")
 | |
|             print("got from stream: ", res)
 | |
|             if res.body.iq:
 | |
|                 jingles = res.body.iq.findAll("jingle")
 | |
|                 if len(jingles):
 | |
|                     self.callfrom = res.body.iq["from"]
 | |
|                     self.handleInvite(jingles[0])
 | |
|             elif "type" in res.body and res.body["type"] == "terminate":
 | |
|                 self.running = False
 | |
|                 del xmppClients[self.matrixRoom]
 | |
|                 return
 | |
| 
 | |
|     def handleInvite(self, jingle):
 | |
|         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))
 | |
|         print("transformed remote offer sdp", sdp)
 | |
|         inviteEvent = {
 | |
|             "offer": {"type": "offer", "sdp": sdp},
 | |
|             "call_id": self.matrixCallId,
 | |
|             "version": 0,
 | |
|             "lifetime": 30000,
 | |
|         }
 | |
|         matrixCli.sendEvent(self.matrixRoom, "m.call.invite", inviteEvent)
 | |
| 
 | |
| 
 | |
| matrixCli = TrivialMatrixClient(ACCESS_TOKEN)  # Undefined name
 | |
| 
 | |
| gevent.joinall([gevent.spawn(matrixLoop)])
 |