prepare for OPENBRIDGE support
This commit is contained in:
parent
285eec5267
commit
f937581e49
254
full_bridge.py
254
full_bridge.py
@ -938,7 +938,7 @@ time_20 = 630720000
|
|||||||
# Build a UNIT_MAP based on values in STATIC_MAP.
|
# Build a UNIT_MAP based on values in STATIC_MAP.
|
||||||
try:
|
try:
|
||||||
for i in STATIC_UNIT:
|
for i in STATIC_UNIT:
|
||||||
UNIT_MAP[bytes_3(i[0])] = i[1], time() + time_20, i[2]
|
UNIT_MAP[bytes_3(i[0])] = i[1], time() + time_20, i[2]
|
||||||
# If empty, return empty dictionary
|
# If empty, return empty dictionary
|
||||||
except:
|
except:
|
||||||
UNIT_MAP = {}
|
UNIT_MAP = {}
|
||||||
@ -1097,7 +1097,7 @@ class routerOBP(OPENBRIDGE):
|
|||||||
|
|
||||||
# list of self._targets for unit (subscriber, private) calls
|
# list of self._targets for unit (subscriber, private) calls
|
||||||
self._targets = []
|
self._targets = []
|
||||||
|
# (self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data)
|
||||||
def group_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _frame_type, _dtype_vseq, _stream_id, _data):
|
def group_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _frame_type, _dtype_vseq, _stream_id, _data):
|
||||||
pkt_time = time()
|
pkt_time = time()
|
||||||
dmrpkt = _data[20:53]
|
dmrpkt = _data[20:53]
|
||||||
@ -1447,6 +1447,256 @@ class routerOBP(OPENBRIDGE):
|
|||||||
if CONFIG['REPORTS']['REPORT']:
|
if CONFIG['REPORTS']['REPORT']:
|
||||||
self._report.send_bridgeEvent('UNIT VOICE,END,RX,{},{},{},{},{},{},{:.2f}'.format(self._system, int_id(_stream_id), int_id(_peer_id), int_id(_rf_src), _slot, int_id(_dst_id), call_duration).encode(encoding='utf-8', errors='ignore'))
|
self._report.send_bridgeEvent('UNIT VOICE,END,RX,{},{},{},{},{},{},{:.2f}'.format(self._system, int_id(_stream_id), int_id(_peer_id), int_id(_rf_src), _slot, int_id(_dst_id), call_duration).encode(encoding='utf-8', errors='ignore'))
|
||||||
|
|
||||||
|
##### DMR data function ####
|
||||||
|
def data_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data):
|
||||||
|
# Capture data headers
|
||||||
|
global n_packet_assembly, hdr_type
|
||||||
|
#logger.info(_dtype_vseq)
|
||||||
|
#logger.info(_call_type)
|
||||||
|
#logger.info(_frame_type)
|
||||||
|
logger.info(strftime('%H:%M:%S - %m/%d/%y'))
|
||||||
|
#logger.info('Special debug for developement:')
|
||||||
|
logger.info(ahex(bptc_decode(_data)))
|
||||||
|
#logger.info(_rf_src)
|
||||||
|
#logger.info((ba2num(bptc_decode(_data)[8:12])))
|
||||||
|
################################################################3###### CHNGED #########
|
||||||
|
if int_id(_dst_id) == data_id:
|
||||||
|
#logger.info(type(_seq))
|
||||||
|
if type(_seq) is bytes:
|
||||||
|
pckt_seq = int.from_bytes(_seq, 'big')
|
||||||
|
else:
|
||||||
|
pckt_seq = _seq
|
||||||
|
# Try to classify header
|
||||||
|
# UDT header has DPF of 0101, which is 5.
|
||||||
|
# If 5 is at position 3, then this should be a UDT header for MD-380 type radios.
|
||||||
|
# Coordinates are usually in the very next block after the header, we will discard the rest.
|
||||||
|
#logger.info(ahex(bptc_decode(_data)[0:10]))
|
||||||
|
if _call_type == call_type and header_ID(_data)[3] == '5' and ba2num(bptc_decode(_data)[69:72]) == 0 and ba2num(bptc_decode(_data)[8:12]) == 0 or (_call_type == 'vcsbk' and header_ID(_data)[3] == '5' and ba2num(bptc_decode(_data)[69:72]) == 0 and ba2num(bptc_decode(_data)[8:12]) == 0):
|
||||||
|
global udt_block
|
||||||
|
logger.info('MD-380 type UDT header detected. Very next packet should be location.')
|
||||||
|
hdr_type = '380'
|
||||||
|
if _dtype_vseq == 6 and hdr_type == '380' or _dtype_vseq == 'group' and hdr_type == '380':
|
||||||
|
udt_block = 1
|
||||||
|
if _dtype_vseq == 7 and hdr_type == '380':
|
||||||
|
udt_block = udt_block - 1
|
||||||
|
if udt_block == 0:
|
||||||
|
logger.info('MD-380 type packet. This should contain the GPS location.')
|
||||||
|
logger.info('Packet: ' + str(ahex(bptc_decode(_data))))
|
||||||
|
if ba2num(bptc_decode(_data)[1:2]) == 1:
|
||||||
|
lat_dir = 'N'
|
||||||
|
if ba2num(bptc_decode(_data)[1:2]) == 0:
|
||||||
|
lat_dir = 'S'
|
||||||
|
if ba2num(bptc_decode(_data)[2:3]) == 1:
|
||||||
|
lon_dir = 'E'
|
||||||
|
if ba2num(bptc_decode(_data)[2:3]) == 0:
|
||||||
|
lon_dir = 'W'
|
||||||
|
lat_deg = ba2num(bptc_decode(_data)[11:18])
|
||||||
|
lon_deg = ba2num(bptc_decode(_data)[38:46])
|
||||||
|
lat_min = ba2num(bptc_decode(_data)[18:24])
|
||||||
|
lon_min = ba2num(bptc_decode(_data)[46:52])
|
||||||
|
lat_min_dec = str(ba2num(bptc_decode(_data)[24:38])).zfill(4)
|
||||||
|
lon_min_dec = str(ba2num(bptc_decode(_data)[52:66])).zfill(4)
|
||||||
|
# Old MD-380 coordinate format, keep here until new is confirmed working.
|
||||||
|
#aprs_lat = str(str(lat_deg) + str(lat_min) + '.' + str(lat_min_dec)[0:2]).zfill(7) + lat_dir
|
||||||
|
#aprs_lon = str(str(lon_deg) + str(lon_min) + '.' + str(lon_min_dec)[0:2]).zfill(8) + lon_dir
|
||||||
|
# Fix for MD-380 by G7HIF
|
||||||
|
aprs_lat = str(str(lat_deg) + str(lat_min).zfill(2) + '.' + str(lat_min_dec)[0:2]).zfill(7) + lat_dir
|
||||||
|
aprs_lon = str(str(lon_deg) + str(lon_min).zfill(2) + '.' + str(lon_min_dec)[0:2]).zfill(8) + lon_dir
|
||||||
|
|
||||||
|
# Form APRS packet
|
||||||
|
#logger.info(aprs_loc_packet)
|
||||||
|
logger.info('Lat: ' + str(aprs_lat) + ' Lon: ' + str(aprs_lon))
|
||||||
|
# 14FRS2013 simplified and moved settings retrieval
|
||||||
|
user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read())
|
||||||
|
if int_id(_rf_src) not in user_settings:
|
||||||
|
ssid = str(user_ssid)
|
||||||
|
icon_table = '/'
|
||||||
|
icon_icon = '['
|
||||||
|
comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src))
|
||||||
|
else:
|
||||||
|
if user_settings[int_id(_rf_src)][1]['ssid'] == '':
|
||||||
|
ssid = user_ssid
|
||||||
|
if user_settings[int_id(_rf_src)][3]['comment'] == '':
|
||||||
|
comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src))
|
||||||
|
if user_settings[int_id(_rf_src)][2]['icon'] == '':
|
||||||
|
icon_table = '/'
|
||||||
|
icon_icon = '['
|
||||||
|
if user_settings[int_id(_rf_src)][2]['icon'] != '':
|
||||||
|
icon_table = user_settings[int_id(_rf_src)][2]['icon'][0]
|
||||||
|
icon_icon = user_settings[int_id(_rf_src)][2]['icon'][1]
|
||||||
|
if user_settings[int_id(_rf_src)][1]['ssid'] != '':
|
||||||
|
ssid = user_settings[int_id(_rf_src)][1]['ssid']
|
||||||
|
if user_settings[int_id(_rf_src)][3]['comment'] != '':
|
||||||
|
comment = user_settings[int_id(_rf_src)][3]['comment']
|
||||||
|
aprs_loc_packet = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid + '>APHBL3,TCPIP*:@' + str(datetime.datetime.utcnow().strftime("%H%M%Sh")) + str(aprs_lat) + icon_table + str(aprs_lon) + icon_icon + '/' + str(comment)
|
||||||
|
logger.info(aprs_loc_packet)
|
||||||
|
logger.info('User comment: ' + comment)
|
||||||
|
logger.info('User SSID: ' + ssid)
|
||||||
|
logger.info('User icon: ' + icon_table + icon_icon)
|
||||||
|
# Attempt to prevent malformed packets from being uploaded.
|
||||||
|
try:
|
||||||
|
aprslib.parse(aprs_loc_packet)
|
||||||
|
float(lat_deg) < 91
|
||||||
|
float(lon_deg) < 121
|
||||||
|
aprs_send(aprs_loc_packet)
|
||||||
|
dashboard_loc_write(str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid, aprs_lat, aprs_lon, time(), comment)
|
||||||
|
#logger.info('Sent APRS packet')
|
||||||
|
except Exception as error_exception:
|
||||||
|
logger.info('Error. Failed to send packet. Packet may be malformed.')
|
||||||
|
logger.info(error_exception)
|
||||||
|
logger.info(str(traceback.extract_tb(error_exception.__traceback__)))
|
||||||
|
udt_block = 1
|
||||||
|
hdr_type = ''
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
#NMEA type packets for Anytone like radios.
|
||||||
|
#if _call_type == call_type or (_call_type == 'vcsbk' and pckt_seq > 3): #int.from_bytes(_seq, 'big') > 3 ):
|
||||||
|
# 14FRS2013 contributed improved header filtering, KF7EEL added conditions to allow both call types at the same time
|
||||||
|
if _call_type == call_type or (_call_type == 'vcsbk' and pckt_seq > 3 and call_type != 'unit') or (_call_type == 'group' and pckt_seq > 3 and call_type != 'unit') or (_call_type == 'group' and pckt_seq > 3 and call_type == 'both') or (_call_type == 'vcsbk' and pckt_seq > 3 and call_type == 'both') or (_call_type == 'unit' and pckt_seq > 3 and call_type == 'both'): #int.from_bytes(_seq, 'big') > 3 ):
|
||||||
|
global packet_assembly, btf
|
||||||
|
if _dtype_vseq == 6 or _dtype_vseq == 'group':
|
||||||
|
global btf, hdr_start
|
||||||
|
hdr_start = str(header_ID(_data))
|
||||||
|
logger.info('Header from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + '. DMR ID: ' + str(int_id(_rf_src)))
|
||||||
|
logger.info(ahex(bptc_decode(_data)))
|
||||||
|
logger.info('Blocks to follow: ' + str(ba2num(bptc_decode(_data)[65:72])))
|
||||||
|
btf = ba2num(bptc_decode(_data)[65:72])
|
||||||
|
# Try resetting packet_assembly
|
||||||
|
packet_assembly = ''
|
||||||
|
# Data blocks at 1/2 rate, see https://github.com/g4klx/MMDVM/blob/master/DMRDefines.h for data types. _dtype_seq defined here also
|
||||||
|
if _dtype_vseq == 7:
|
||||||
|
btf = btf - 1
|
||||||
|
logger.info('Block #: ' + str(btf))
|
||||||
|
#logger.info(_seq)
|
||||||
|
logger.info('Data block from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + '. DMR ID: ' + str(int_id(_rf_src)) + '. Destination: ' + str(int_id(_dst_id)))
|
||||||
|
logger.info(ahex(bptc_decode(_data)))
|
||||||
|
if _seq == 0:
|
||||||
|
n_packet_assembly = 0
|
||||||
|
packet_assembly = ''
|
||||||
|
|
||||||
|
#if btf < btf + 1:
|
||||||
|
# 14FRS2013 removed condition, works great!
|
||||||
|
n_packet_assembly = n_packet_assembly + 1
|
||||||
|
packet_assembly = packet_assembly + str(bptc_decode(_data)) #str((decode_full_lc(b_packet)).strip('bitarray('))
|
||||||
|
# Use block 0 as trigger. $GPRMC must also be in string to indicate NMEA.
|
||||||
|
# This triggers the APRS upload
|
||||||
|
if btf == 0:
|
||||||
|
final_packet = str(bitarray(re.sub("\)|\(|bitarray|'", '', packet_assembly)).tobytes().decode('utf-8', 'ignore'))
|
||||||
|
sms_hex = str(ba2hx(bitarray(re.sub("\)|\(|bitarray|'", '', packet_assembly))))
|
||||||
|
sms_hex_string = re.sub("b'|'", '', str(sms_hex))
|
||||||
|
#NMEA GPS sentence
|
||||||
|
if '$GPRMC' in final_packet or '$GNRMC' in final_packet:
|
||||||
|
logger.info(final_packet + '\n')
|
||||||
|
# Eliminate excess bytes based on NMEA type
|
||||||
|
# GPRMC
|
||||||
|
if 'GPRMC' in final_packet:
|
||||||
|
logger.info('GPRMC location')
|
||||||
|
#nmea_parse = re.sub('A\*.*|.*\$', '', str(final_packet))
|
||||||
|
nmea_parse = re.sub('A\*.*|.*\$|\n.*', '', str(final_packet))
|
||||||
|
# GNRMC
|
||||||
|
if 'GNRMC' in final_packet:
|
||||||
|
logger.info('GNRMC location')
|
||||||
|
nmea_parse = re.sub('.*\$|\n.*|V\*.*', '', final_packet)
|
||||||
|
loc = pynmea2.parse(nmea_parse, check=False)
|
||||||
|
logger.info('Latitude: ' + str(loc.lat) + str(loc.lat_dir) + ' Longitude: ' + str(loc.lon) + str(loc.lon_dir) + ' Direction: ' + str(loc.true_course) + ' Speed: ' + str(loc.spd_over_grnd) + '\n')
|
||||||
|
try:
|
||||||
|
# Begin APRS format and upload
|
||||||
|
# Disable opening file for reading to reduce "collision" or reading and writing at same time.
|
||||||
|
# 14FRS2013 simplified and moved settings retrieval
|
||||||
|
user_settings = ast.literal_eval(os.popen('cat ' + user_settings_file).read())
|
||||||
|
if int_id(_rf_src) not in user_settings:
|
||||||
|
ssid = str(user_ssid)
|
||||||
|
icon_table = '/'
|
||||||
|
icon_icon = '['
|
||||||
|
comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src))
|
||||||
|
else:
|
||||||
|
if user_settings[int_id(_rf_src)][1]['ssid'] == '':
|
||||||
|
ssid = user_ssid
|
||||||
|
if user_settings[int_id(_rf_src)][3]['comment'] == '':
|
||||||
|
comment = aprs_comment + ' DMR ID: ' + str(int_id(_rf_src))
|
||||||
|
if user_settings[int_id(_rf_src)][2]['icon'] == '':
|
||||||
|
icon_table = '/'
|
||||||
|
icon_icon = '['
|
||||||
|
if user_settings[int_id(_rf_src)][2]['icon'] != '':
|
||||||
|
icon_table = user_settings[int_id(_rf_src)][2]['icon'][0]
|
||||||
|
icon_icon = user_settings[int_id(_rf_src)][2]['icon'][1]
|
||||||
|
if user_settings[int_id(_rf_src)][1]['ssid'] != '':
|
||||||
|
ssid = user_settings[int_id(_rf_src)][1]['ssid']
|
||||||
|
if user_settings[int_id(_rf_src)][3]['comment'] != '':
|
||||||
|
comment = user_settings[int_id(_rf_src)][3]['comment']
|
||||||
|
aprs_loc_packet = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid + '>APHBL3,TCPIP*:@' + str(datetime.datetime.utcnow().strftime("%H%M%Sh")) + str(loc.lat[0:7]) + str(loc.lat_dir) + icon_table + str(loc.lon[0:8]) + str(loc.lon_dir) + icon_icon + str(round(loc.true_course)).zfill(3) + '/' + str(round(loc.spd_over_grnd)).zfill(3) + '/' + str(comment)
|
||||||
|
logger.info(aprs_loc_packet)
|
||||||
|
logger.info('User comment: ' + comment)
|
||||||
|
logger.info('User SSID: ' + ssid)
|
||||||
|
logger.info('User icon: ' + icon_table + icon_icon)
|
||||||
|
except Exception as error_exception:
|
||||||
|
logger.info('Error or user settings file not found, proceeding with default settings.')
|
||||||
|
aprs_loc_packet = str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + str(user_ssid) + '>APHBL3,TCPIP*:@' + str(datetime.datetime.utcnow().strftime("%H%M%Sh")) + str(loc.lat[0:7]) + str(loc.lat_dir) + '/' + str(loc.lon[0:8]) + str(loc.lon_dir) + '[' + str(round(loc.true_course)).zfill(3) + '/' + str(round(loc.spd_over_grnd)).zfill(3) + '/' + aprs_comment + ' DMR ID: ' + str(int_id(_rf_src))
|
||||||
|
logger.info(error_exception)
|
||||||
|
logger.info(str(traceback.extract_tb(error_exception.__traceback__)))
|
||||||
|
try:
|
||||||
|
# Try parse of APRS packet. If it fails, it will not upload to APRS-IS
|
||||||
|
aprslib.parse(aprs_loc_packet)
|
||||||
|
# Float values of lat and lon. Anything that is not a number will cause it to fail.
|
||||||
|
float(loc.lat)
|
||||||
|
float(loc.lon)
|
||||||
|
aprs_send(aprs_loc_packet)
|
||||||
|
dashboard_loc_write(str(get_alias(int_id(_rf_src), subscriber_ids)) + '-' + ssid, str(loc.lat[0:7]) + str(loc.lat_dir), str(loc.lon[0:8]) + str(loc.lon_dir), time(), comment)
|
||||||
|
except Exception as error_exception:
|
||||||
|
logger.info('Failed to parse packet. Packet may be deformed. Not uploaded.')
|
||||||
|
logger.info(error_exception)
|
||||||
|
logger.info(str(traceback.extract_tb(error_exception.__traceback__)))
|
||||||
|
# Get callsign based on DMR ID
|
||||||
|
# End APRS-IS upload
|
||||||
|
# Assume this is an SMS message
|
||||||
|
elif '$GPRMC' not in final_packet or '$GNRMC' not in final_packet:
|
||||||
|
|
||||||
|
#### # Motorola type SMS header
|
||||||
|
## if '824a' in hdr_start or '024a' in hdr_start:
|
||||||
|
## logger.info('\nMotorola type SMS')
|
||||||
|
## sms = codecs.decode(bytes.fromhex(''.join(sms_hex[74:-8].split('00'))), 'utf-8')
|
||||||
|
## logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(sms) + '\n')
|
||||||
|
## process_sms(_rf_src, sms)
|
||||||
|
## packet_assembly = ''
|
||||||
|
## # ETSI? type SMS header
|
||||||
|
## elif '0244' in hdr_start or '8244' in hdr_start:
|
||||||
|
## logger.info('ETSI? type SMS')
|
||||||
|
## sms = codecs.decode(bytes.fromhex(''.join(sms_hex[64:-8].split('00'))), 'utf-8')
|
||||||
|
## logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(sms) + '\n')
|
||||||
|
## #logger.info(final_packet)
|
||||||
|
## #logger.info(sms_hex[64:-8])
|
||||||
|
## process_sms(_rf_src, sms)
|
||||||
|
## packet_assembly = ''
|
||||||
|
####
|
||||||
|
## else:
|
||||||
|
logger.info('\nSMS detected. Attempting to parse.')
|
||||||
|
#logger.info(final_packet)
|
||||||
|
logger.info(sms_hex)
|
||||||
|
## logger.info(type(sms_hex))
|
||||||
|
logger.info('Attempting to find command...')
|
||||||
|
## sms = codecs.decode(bytes.fromhex(''.join(sms_hex[:-8].split('00'))), 'utf-8', 'ignore')
|
||||||
|
sms = codecs.decode(bytes.fromhex(''.join(sms_hex_string[:-8].split('00'))), 'utf-8', 'ignore')
|
||||||
|
msg_found = re.sub('.*\n', '', sms)
|
||||||
|
logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(msg_found) + '\n')
|
||||||
|
process_sms(_rf_src, msg_found)
|
||||||
|
#packet_assembly = ''
|
||||||
|
pass
|
||||||
|
#logger.info(bitarray(re.sub("\)|\(|bitarray|'", '', str(bptc_decode(_data)).tobytes().decode('utf-8', 'ignore'))))
|
||||||
|
#logger.info('\n\n' + 'Received SMS from ' + str(get_alias(int_id(_rf_src), subscriber_ids)) + ', DMR ID: ' + str(int_id(_rf_src)) + ': ' + str(sms) + '\n')
|
||||||
|
# Reset the packet assembly to prevent old data from returning.
|
||||||
|
# 14FRS2013 moved variable reset
|
||||||
|
hdr_start = ''
|
||||||
|
n_packet_assembly = 0
|
||||||
|
packet_assembly = ''
|
||||||
|
btf = 0
|
||||||
|
#logger.info(_seq)
|
||||||
|
#packet_assembly = '' #logger.info(_dtype_vseq)
|
||||||
|
#logger.info(ahex(bptc_decode(_data)).decode('utf-8', 'ignore'))
|
||||||
|
#logger.info(bitarray(re.sub("\)|\(|bitarray|'", '', str(bptc_decode(_data)).tobytes().decode('utf-8', 'ignore'))))
|
||||||
|
|
||||||
|
|
||||||
|
######
|
||||||
|
|
||||||
|
|
||||||
def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data):
|
def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data):
|
||||||
|
|
||||||
|
@ -62,10 +62,11 @@ UNIT_TIME = 1
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
Input the DMR ID and SYSTEM of a subscriber that you would like to have always have private calls routed.
|
Input the DMR ID and SYSTEM of a subscriber that you would like to have always have private calls routed.
|
||||||
This will not flood all systems.
|
This will not flood all systems. The last item in the entry is the timeslot. It has no effect on STATIC_UNIT. It
|
||||||
|
is only there to prevent an error in another function.
|
||||||
'''
|
'''
|
||||||
STATIC_UNIT = [
|
STATIC_UNIT = [
|
||||||
# [ 9099, 'MASTER-1']
|
# [ 9099, 'MASTER-1', 2]
|
||||||
]
|
]
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
Loading…
Reference in New Issue
Block a user