mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-10-01 11:49:51 -04:00
convert to spaces before I start a holy war
This commit is contained in:
parent
7642d95d5e
commit
bdc21e7282
@ -39,43 +39,43 @@ ROOMDOMAIN="meet.jit.si"
|
|||||||
#ROOMDOMAIN="conference.jitsi.vuc.me"
|
#ROOMDOMAIN="conference.jitsi.vuc.me"
|
||||||
|
|
||||||
class TrivialMatrixClient:
|
class TrivialMatrixClient:
|
||||||
def __init__(self, access_token):
|
def __init__(self, access_token):
|
||||||
self.token = None
|
self.token = None
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
|
|
||||||
def getEvent(self):
|
def getEvent(self):
|
||||||
while True:
|
while True:
|
||||||
url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
|
url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
|
||||||
if self.token:
|
if self.token:
|
||||||
url += "&from="+self.token
|
url += "&from="+self.token
|
||||||
req = grequests.get(url)
|
req = grequests.get(url)
|
||||||
resps = grequests.map([req])
|
resps = grequests.map([req])
|
||||||
obj = json.loads(resps[0].content)
|
obj = json.loads(resps[0].content)
|
||||||
print "incoming from matrix",obj
|
print "incoming from matrix",obj
|
||||||
if 'end' not in obj:
|
if 'end' not in obj:
|
||||||
continue
|
continue
|
||||||
self.token = obj['end']
|
self.token = obj['end']
|
||||||
if len(obj['chunk']):
|
if len(obj['chunk']):
|
||||||
return obj['chunk'][0]
|
return obj['chunk'][0]
|
||||||
|
|
||||||
def joinRoom(self, roomId):
|
def joinRoom(self, roomId):
|
||||||
url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
|
url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
|
||||||
print url
|
print url
|
||||||
headers={ 'Content-Type': 'application/json' }
|
headers={ 'Content-Type': 'application/json' }
|
||||||
req = grequests.post(url, headers=headers, data='{}')
|
req = grequests.post(url, headers=headers, data='{}')
|
||||||
resps = grequests.map([req])
|
resps = grequests.map([req])
|
||||||
obj = json.loads(resps[0].content)
|
obj = json.loads(resps[0].content)
|
||||||
print "response: ",obj
|
print "response: ",obj
|
||||||
|
|
||||||
def sendEvent(self, roomId, evType, event):
|
def sendEvent(self, roomId, evType, event):
|
||||||
url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
|
url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
|
||||||
print url
|
print url
|
||||||
print json.dumps(event)
|
print json.dumps(event)
|
||||||
headers={ 'Content-Type': 'application/json' }
|
headers={ 'Content-Type': 'application/json' }
|
||||||
req = grequests.post(url, headers=headers, data=json.dumps(event))
|
req = grequests.post(url, headers=headers, data=json.dumps(event))
|
||||||
resps = grequests.map([req])
|
resps = grequests.map([req])
|
||||||
obj = json.loads(resps[0].content)
|
obj = json.loads(resps[0].content)
|
||||||
print "response: ",obj
|
print "response: ",obj
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -83,178 +83,178 @@ xmppClients = {}
|
|||||||
|
|
||||||
|
|
||||||
def matrixLoop():
|
def matrixLoop():
|
||||||
while True:
|
while True:
|
||||||
ev = matrixCli.getEvent()
|
ev = matrixCli.getEvent()
|
||||||
print ev
|
print ev
|
||||||
if ev['type'] == 'm.room.member':
|
if ev['type'] == 'm.room.member':
|
||||||
print 'membership event'
|
print 'membership event'
|
||||||
if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
|
if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
|
||||||
roomId = ev['room_id']
|
roomId = ev['room_id']
|
||||||
print "joining room %s" % (roomId)
|
print "joining room %s" % (roomId)
|
||||||
matrixCli.joinRoom(roomId)
|
matrixCli.joinRoom(roomId)
|
||||||
elif ev['type'] == 'm.room.message':
|
elif ev['type'] == 'm.room.message':
|
||||||
if ev['room_id'] in xmppClients:
|
if ev['room_id'] in xmppClients:
|
||||||
print "already have a bridge for that user, ignoring"
|
print "already have a bridge for that user, ignoring"
|
||||||
continue
|
continue
|
||||||
print "got message, connecting"
|
print "got message, connecting"
|
||||||
xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
||||||
gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
||||||
elif ev['type'] == 'm.call.invite':
|
elif ev['type'] == 'm.call.invite':
|
||||||
print "Incoming call"
|
print "Incoming call"
|
||||||
#sdp = ev['content']['offer']['sdp']
|
#sdp = ev['content']['offer']['sdp']
|
||||||
#print "sdp: %s" % (sdp)
|
#print "sdp: %s" % (sdp)
|
||||||
#xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
#xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
||||||
#gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
#gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
||||||
elif ev['type'] == 'm.call.answer':
|
elif ev['type'] == 'm.call.answer':
|
||||||
print "Call answered"
|
print "Call answered"
|
||||||
sdp = ev['content']['answer']['sdp']
|
sdp = ev['content']['answer']['sdp']
|
||||||
if ev['room_id'] not in xmppClients:
|
if ev['room_id'] not in xmppClients:
|
||||||
print "We didn't have a call for that room"
|
print "We didn't have a call for that room"
|
||||||
continue
|
continue
|
||||||
# should probably check call ID too
|
# should probably check call ID too
|
||||||
xmppCli = xmppClients[ev['room_id']]
|
xmppCli = xmppClients[ev['room_id']]
|
||||||
xmppCli.sendAnswer(sdp)
|
xmppCli.sendAnswer(sdp)
|
||||||
elif ev['type'] == 'm.call.hangup':
|
elif ev['type'] == 'm.call.hangup':
|
||||||
if ev['room_id'] in xmppClients:
|
if ev['room_id'] in xmppClients:
|
||||||
xmppClients[ev['room_id']].stop()
|
xmppClients[ev['room_id']].stop()
|
||||||
del xmppClients[ev['room_id']]
|
del xmppClients[ev['room_id']]
|
||||||
|
|
||||||
class TrivialXmppClient:
|
class TrivialXmppClient:
|
||||||
def __init__(self, matrixRoom, userId):
|
def __init__(self, matrixRoom, userId):
|
||||||
self.rid = 0
|
self.rid = 0
|
||||||
self.matrixRoom = matrixRoom
|
self.matrixRoom = matrixRoom
|
||||||
self.userId = userId
|
self.userId = userId
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
def nextRid(self):
|
def nextRid(self):
|
||||||
self.rid += 1
|
self.rid += 1
|
||||||
return '%d' % (self.rid)
|
return '%d' % (self.rid)
|
||||||
|
|
||||||
def sendIq(self, xml):
|
def sendIq(self, xml):
|
||||||
fullXml = "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>" % (self.nextRid(), self.sid, xml)
|
fullXml = "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>" % (self.nextRid(), self.sid, xml)
|
||||||
#print "\t>>>%s" % (fullXml)
|
#print "\t>>>%s" % (fullXml)
|
||||||
return self.xmppPoke(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):
|
def xmppPoke(self, xml):
|
||||||
print "sdp from matrix client",answer
|
headers = {'Content-Type': 'application/xml'}
|
||||||
p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
|
||||||
jingle, out_err = p.communicate(answer)
|
resps = grequests.map([req])
|
||||||
jingle = jingle % {
|
obj = BeautifulSoup(resps[0].content)
|
||||||
'tojid': self.callfrom,
|
return obj
|
||||||
'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([
|
def sendAnswer(self, answer):
|
||||||
gevent.spawn(self.advertiseSsrcs)
|
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)
|
||||||
def advertiseSsrcs(self):
|
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)
|
time.sleep(7)
|
||||||
print "SSRC spammer started"
|
print "SSRC spammer started"
|
||||||
while self.running:
|
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'] }
|
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)
|
res = self.sendIq(ssrcMsg)
|
||||||
print "reply from ssrc announce: ",res
|
print "reply from ssrc announce: ",res
|
||||||
time.sleep(10)
|
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'/>")
|
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))
|
||||||
|
|
||||||
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))
|
print res
|
||||||
|
self.sid = res.body['sid']
|
||||||
res = self.sendIq("<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>")
|
print "sid %s" % (self.sid)
|
||||||
print res
|
|
||||||
|
|
||||||
self.jid = res.body.iq.bind.jid.string
|
res = self.sendIq("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>")
|
||||||
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>")
|
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))
|
||||||
|
|
||||||
#randomthing = res.body.iq['to']
|
res = self.sendIq("<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>")
|
||||||
#whatsitpart = randomthing.split('-')[0]
|
print res
|
||||||
|
|
||||||
#print "other random bind thing: %s" % (randomthing)
|
self.jid = res.body.iq.bind.jid.string
|
||||||
|
print "jid: %s" % (self.jid)
|
||||||
|
self.shortJid = self.jid.split('-')[0]
|
||||||
|
|
||||||
# advertise preence to the jitsi room, with our nick
|
res = self.sendIq("<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>")
|
||||||
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
|
#randomthing = res.body.iq['to']
|
||||||
while True:
|
#whatsitpart = randomthing.split('-')[0]
|
||||||
print "waiting..."
|
|
||||||
res = self.sendIq("")
|
#print "other random bind thing: %s" % (randomthing)
|
||||||
print "got from stream: ",res
|
|
||||||
if res.body.iq:
|
# advertise preence to the jitsi room, with our nick
|
||||||
jingles = res.body.iq.findAll('jingle')
|
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))
|
||||||
if len(jingles):
|
self.muc = {'users': []}
|
||||||
self.callfrom = res.body.iq['from']
|
for p in res.body.findAll('presence'):
|
||||||
self.handleInvite(jingles[0])
|
u = {}
|
||||||
elif 'type' in res.body and res.body['type'] == 'terminate':
|
u['shortJid'] = p['from'].split('/')[1]
|
||||||
self.running = False
|
if p.c and p.c.nick:
|
||||||
del xmppClients[self.matrixRoom]
|
u['nick'] = p.c.nick.string
|
||||||
return
|
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)
|
||||||
|
|
||||||
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)
|
matrixCli = TrivialMatrixClient(ACCESS_TOKEN)
|
||||||
|
|
||||||
gevent.joinall([
|
gevent.joinall([
|
||||||
gevent.spawn(matrixLoop)
|
gevent.spawn(matrixLoop)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user