NFC refactoring (#3050)
"A long time ago in a galaxy far, far away...." we started NFC subsystem refactoring. Starring: - @gornekich - NFC refactoring project lead, architect, senior developer - @gsurkov - architect, senior developer - @RebornedBrain - senior developer Supporting roles: - @skotopes, @DrZlo13, @hedger - general architecture advisors, code review - @Astrrra, @doomwastaken, @Hellitron, @ImagineVagon333 - quality assurance Special thanks: @bettse, @pcunning, @nxv, @noproto, @AloneLiberty and everyone else who has been helping us all this time and contributing valuable knowledges, ideas and source code.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#include "felica.h"
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
#include <nfc/nfc_common.h>
|
||||
|
||||
#define FELICA_PROTOCOL_NAME "FeliCa"
|
||||
#define FELICA_DEVICE_NAME "FeliCa"
|
||||
|
||||
#define FELICA_DATA_FORMAT_VERSION "Data format version"
|
||||
#define FELICA_MANUFACTURE_ID "Manufacture id"
|
||||
#define FELICA_MANUFACTURE_PARAMETER "Manufacture parameter"
|
||||
|
||||
static const uint32_t felica_data_format_version = 1;
|
||||
|
||||
const NfcDeviceBase nfc_device_felica = {
|
||||
.protocol_name = FELICA_PROTOCOL_NAME,
|
||||
.alloc = (NfcDeviceAlloc)felica_alloc,
|
||||
.free = (NfcDeviceFree)felica_free,
|
||||
.reset = (NfcDeviceReset)felica_reset,
|
||||
.copy = (NfcDeviceCopy)felica_copy,
|
||||
.verify = (NfcDeviceVerify)felica_verify,
|
||||
.load = (NfcDeviceLoad)felica_load,
|
||||
.save = (NfcDeviceSave)felica_save,
|
||||
.is_equal = (NfcDeviceEqual)felica_is_equal,
|
||||
.get_name = (NfcDeviceGetName)felica_get_device_name,
|
||||
.get_uid = (NfcDeviceGetUid)felica_get_uid,
|
||||
.set_uid = (NfcDeviceSetUid)felica_set_uid,
|
||||
.get_base_data = (NfcDeviceGetBaseData)felica_get_base_data,
|
||||
};
|
||||
|
||||
FelicaData* felica_alloc() {
|
||||
FelicaData* data = malloc(sizeof(FelicaData));
|
||||
return data;
|
||||
}
|
||||
|
||||
void felica_free(FelicaData* data) {
|
||||
furi_assert(data);
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
void felica_reset(FelicaData* data) {
|
||||
memset(data, 0, sizeof(FelicaData));
|
||||
}
|
||||
|
||||
void felica_copy(FelicaData* data, const FelicaData* other) {
|
||||
furi_assert(data);
|
||||
furi_assert(other);
|
||||
|
||||
*data = *other;
|
||||
}
|
||||
|
||||
bool felica_verify(FelicaData* data, const FuriString* device_type) {
|
||||
UNUSED(data);
|
||||
UNUSED(device_type);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) {
|
||||
furi_assert(data);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
if(version < NFC_UNIFIED_FORMAT_VERSION) break;
|
||||
|
||||
uint32_t data_format_version = 0;
|
||||
if(!flipper_format_read_uint32(ff, FELICA_DATA_FORMAT_VERSION, &data_format_version, 1))
|
||||
break;
|
||||
if(data_format_version != felica_data_format_version) break;
|
||||
if(!flipper_format_read_hex(ff, FELICA_MANUFACTURE_ID, data->idm.data, FELICA_IDM_SIZE))
|
||||
break;
|
||||
if(!flipper_format_read_hex(
|
||||
ff, FELICA_MANUFACTURE_PARAMETER, data->pmm.data, FELICA_PMM_SIZE))
|
||||
break;
|
||||
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
bool felica_save(const FelicaData* data, FlipperFormat* ff) {
|
||||
furi_assert(data);
|
||||
|
||||
bool saved = false;
|
||||
|
||||
do {
|
||||
if(!flipper_format_write_comment_cstr(ff, FELICA_PROTOCOL_NAME " specific data")) break;
|
||||
if(!flipper_format_write_uint32(
|
||||
ff, FELICA_DATA_FORMAT_VERSION, &felica_data_format_version, 1))
|
||||
break;
|
||||
if(!flipper_format_write_hex(ff, FELICA_MANUFACTURE_ID, data->idm.data, FELICA_IDM_SIZE))
|
||||
break;
|
||||
if(!flipper_format_write_hex(
|
||||
ff, FELICA_MANUFACTURE_PARAMETER, data->pmm.data, FELICA_PMM_SIZE))
|
||||
break;
|
||||
|
||||
saved = true;
|
||||
} while(false);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
bool felica_is_equal(const FelicaData* data, const FelicaData* other) {
|
||||
furi_assert(data);
|
||||
furi_assert(other);
|
||||
|
||||
return memcmp(data, other, sizeof(FelicaData)) == 0;
|
||||
}
|
||||
|
||||
const char* felica_get_device_name(const FelicaData* data, NfcDeviceNameType name_type) {
|
||||
UNUSED(data);
|
||||
UNUSED(name_type);
|
||||
|
||||
return FELICA_DEVICE_NAME;
|
||||
}
|
||||
|
||||
const uint8_t* felica_get_uid(const FelicaData* data, size_t* uid_len) {
|
||||
furi_assert(data);
|
||||
|
||||
// Consider Manufacturer ID as UID
|
||||
if(uid_len) {
|
||||
*uid_len = FELICA_IDM_SIZE;
|
||||
}
|
||||
|
||||
return data->idm.data;
|
||||
}
|
||||
|
||||
bool felica_set_uid(FelicaData* data, const uint8_t* uid, size_t uid_len) {
|
||||
furi_assert(data);
|
||||
|
||||
// Consider Manufacturer ID as UID
|
||||
const bool uid_valid = uid_len == FELICA_IDM_SIZE;
|
||||
if(uid_valid) {
|
||||
memcpy(data->idm.data, uid, uid_len);
|
||||
}
|
||||
|
||||
return uid_valid;
|
||||
}
|
||||
|
||||
FelicaData* felica_get_base_data(const FelicaData* data) {
|
||||
UNUSED(data);
|
||||
furi_crash("No base data");
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <toolbox/bit_buffer.h>
|
||||
#include <nfc/protocols/nfc_device_base_i.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define FELICA_IDM_SIZE (8U)
|
||||
#define FELICA_PMM_SIZE (8U)
|
||||
|
||||
#define FELICA_GUARD_TIME_US (20000U)
|
||||
#define FELICA_FDT_POLL_FC (10000U)
|
||||
#define FELICA_POLL_POLL_MIN_US (1280U)
|
||||
|
||||
#define FELICA_SYSTEM_CODE_CODE (0xFFFFU)
|
||||
#define FELICA_TIME_SLOT_1 (0x00U)
|
||||
#define FELICA_TIME_SLOT_2 (0x01U)
|
||||
#define FELICA_TIME_SLOT_4 (0x03U)
|
||||
#define FELICA_TIME_SLOT_8 (0x07U)
|
||||
#define FELICA_TIME_SLOT_16 (0x0FU)
|
||||
|
||||
typedef enum {
|
||||
FelicaErrorNone,
|
||||
FelicaErrorNotPresent,
|
||||
FelicaErrorColResFailed,
|
||||
FelicaErrorBufferOverflow,
|
||||
FelicaErrorCommunication,
|
||||
FelicaErrorFieldOff,
|
||||
FelicaErrorWrongCrc,
|
||||
FelicaErrorProtocol,
|
||||
FelicaErrorTimeout,
|
||||
} FelicaError;
|
||||
|
||||
typedef struct {
|
||||
uint8_t data[FELICA_IDM_SIZE];
|
||||
} FelicaIDm;
|
||||
|
||||
typedef struct {
|
||||
uint8_t data[FELICA_PMM_SIZE];
|
||||
} FelicaPMm;
|
||||
|
||||
typedef struct {
|
||||
FelicaIDm idm;
|
||||
FelicaPMm pmm;
|
||||
} FelicaData;
|
||||
|
||||
extern const NfcDeviceBase nfc_device_felica;
|
||||
|
||||
FelicaData* felica_alloc();
|
||||
|
||||
void felica_free(FelicaData* data);
|
||||
|
||||
void felica_reset(FelicaData* data);
|
||||
|
||||
void felica_copy(FelicaData* data, const FelicaData* other);
|
||||
|
||||
bool felica_verify(FelicaData* data, const FuriString* device_type);
|
||||
|
||||
bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version);
|
||||
|
||||
bool felica_save(const FelicaData* data, FlipperFormat* ff);
|
||||
|
||||
bool felica_is_equal(const FelicaData* data, const FelicaData* other);
|
||||
|
||||
const char* felica_get_device_name(const FelicaData* data, NfcDeviceNameType name_type);
|
||||
|
||||
const uint8_t* felica_get_uid(const FelicaData* data, size_t* uid_len);
|
||||
|
||||
bool felica_set_uid(FelicaData* data, const uint8_t* uid, size_t uid_len);
|
||||
|
||||
FelicaData* felica_get_base_data(const FelicaData* data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "felica_poller_i.h"
|
||||
|
||||
#include <nfc/protocols/nfc_poller_base.h>
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
const FelicaData* felica_poller_get_data(FelicaPoller* instance) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->data);
|
||||
|
||||
return instance->data;
|
||||
}
|
||||
|
||||
static FelicaPoller* felica_poller_alloc(Nfc* nfc) {
|
||||
furi_assert(nfc);
|
||||
|
||||
FelicaPoller* instance = malloc(sizeof(FelicaPoller));
|
||||
instance->nfc = nfc;
|
||||
instance->tx_buffer = bit_buffer_alloc(FELICA_POLLER_MAX_BUFFER_SIZE);
|
||||
instance->rx_buffer = bit_buffer_alloc(FELICA_POLLER_MAX_BUFFER_SIZE);
|
||||
|
||||
nfc_config(instance->nfc, NfcModePoller, NfcTechFelica);
|
||||
nfc_set_guard_time_us(instance->nfc, FELICA_GUARD_TIME_US);
|
||||
nfc_set_fdt_poll_fc(instance->nfc, FELICA_FDT_POLL_FC);
|
||||
nfc_set_fdt_poll_poll_us(instance->nfc, FELICA_POLL_POLL_MIN_US);
|
||||
instance->data = felica_alloc();
|
||||
|
||||
instance->felica_event.data = &instance->felica_event_data;
|
||||
instance->general_event.protocol = NfcProtocolFelica;
|
||||
instance->general_event.event_data = &instance->felica_event;
|
||||
instance->general_event.instance = instance;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
static void felica_poller_free(FelicaPoller* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
furi_assert(instance->tx_buffer);
|
||||
furi_assert(instance->rx_buffer);
|
||||
furi_assert(instance->data);
|
||||
|
||||
bit_buffer_free(instance->tx_buffer);
|
||||
bit_buffer_free(instance->rx_buffer);
|
||||
felica_free(instance->data);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
static void
|
||||
felica_poller_set_callback(FelicaPoller* instance, NfcGenericCallback callback, void* context) {
|
||||
furi_assert(instance);
|
||||
furi_assert(callback);
|
||||
|
||||
instance->callback = callback;
|
||||
instance->context = context;
|
||||
}
|
||||
|
||||
static NfcCommand felica_poller_run(NfcGenericEvent event, void* context) {
|
||||
furi_assert(context);
|
||||
furi_assert(event.protocol == NfcProtocolInvalid);
|
||||
furi_assert(event.event_data);
|
||||
|
||||
FelicaPoller* instance = context;
|
||||
NfcEvent* nfc_event = event.event_data;
|
||||
NfcCommand command = NfcCommandContinue;
|
||||
|
||||
if(nfc_event->type == NfcEventTypePollerReady) {
|
||||
if(instance->state != FelicaPollerStateActivated) {
|
||||
FelicaError error = felica_poller_async_activate(instance, instance->data);
|
||||
if(error == FelicaErrorNone) {
|
||||
instance->felica_event.type = FelicaPollerEventTypeReady;
|
||||
instance->felica_event_data.error = error;
|
||||
command = instance->callback(instance->general_event, instance->context);
|
||||
} else {
|
||||
instance->felica_event.type = FelicaPollerEventTypeError;
|
||||
instance->felica_event_data.error = error;
|
||||
command = instance->callback(instance->general_event, instance->context);
|
||||
// Add delay to switch context
|
||||
furi_delay_ms(100);
|
||||
}
|
||||
} else {
|
||||
instance->felica_event.type = FelicaPollerEventTypeReady;
|
||||
instance->felica_event_data.error = FelicaErrorNone;
|
||||
command = instance->callback(instance->general_event, instance->context);
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
static bool felica_poller_detect(NfcGenericEvent event, void* context) {
|
||||
furi_assert(context);
|
||||
furi_assert(event.event_data);
|
||||
furi_assert(event.instance);
|
||||
furi_assert(event.protocol == NfcProtocolInvalid);
|
||||
|
||||
bool protocol_detected = false;
|
||||
FelicaPoller* instance = context;
|
||||
NfcEvent* nfc_event = event.event_data;
|
||||
furi_assert(instance->state == FelicaPollerStateIdle);
|
||||
|
||||
if(nfc_event->type == NfcEventTypePollerReady) {
|
||||
FelicaError error = felica_poller_async_activate(instance, instance->data);
|
||||
protocol_detected = (error == FelicaErrorNone);
|
||||
}
|
||||
|
||||
return protocol_detected;
|
||||
}
|
||||
|
||||
const NfcPollerBase nfc_poller_felica = {
|
||||
.alloc = (NfcPollerAlloc)felica_poller_alloc,
|
||||
.free = (NfcPollerFree)felica_poller_free,
|
||||
.set_callback = (NfcPollerSetCallback)felica_poller_set_callback,
|
||||
.run = (NfcPollerRun)felica_poller_run,
|
||||
.detect = (NfcPollerDetect)felica_poller_detect,
|
||||
.get_data = (NfcPollerGetData)felica_poller_get_data,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "felica.h"
|
||||
#include <lib/nfc/nfc.h>
|
||||
|
||||
#include <nfc/nfc_poller.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct FelicaPoller FelicaPoller;
|
||||
|
||||
typedef enum {
|
||||
FelicaPollerEventTypeError,
|
||||
FelicaPollerEventTypeReady,
|
||||
} FelicaPollerEventType;
|
||||
|
||||
typedef struct {
|
||||
FelicaError error;
|
||||
} FelicaPollerEventData;
|
||||
|
||||
typedef struct {
|
||||
FelicaPollerEventType type;
|
||||
FelicaPollerEventData* data;
|
||||
} FelicaPollerEvent;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <nfc/protocols/nfc_poller_base.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern const NfcPollerBase nfc_poller_felica;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,128 @@
|
||||
#include "felica_poller_i.h"
|
||||
|
||||
#include <nfc/helpers/felica_crc.h>
|
||||
|
||||
#define TAG "FelicaPoller"
|
||||
|
||||
static FelicaError felica_poller_process_error(NfcError error) {
|
||||
switch(error) {
|
||||
case NfcErrorNone:
|
||||
return FelicaErrorNone;
|
||||
case NfcErrorTimeout:
|
||||
return FelicaErrorTimeout;
|
||||
default:
|
||||
return FelicaErrorNotPresent;
|
||||
}
|
||||
}
|
||||
|
||||
static FelicaError felica_poller_frame_exchange(
|
||||
FelicaPoller* instance,
|
||||
const BitBuffer* tx_buffer,
|
||||
BitBuffer* rx_buffer,
|
||||
uint32_t fwt) {
|
||||
furi_assert(instance);
|
||||
|
||||
const size_t tx_bytes = bit_buffer_get_size_bytes(tx_buffer);
|
||||
furi_assert(tx_bytes <= bit_buffer_get_capacity_bytes(instance->tx_buffer) - FELICA_CRC_SIZE);
|
||||
|
||||
felica_crc_append(instance->tx_buffer);
|
||||
|
||||
FelicaError ret = FelicaErrorNone;
|
||||
|
||||
do {
|
||||
NfcError error =
|
||||
nfc_poller_trx(instance->nfc, instance->tx_buffer, instance->rx_buffer, fwt);
|
||||
if(error != NfcErrorNone) {
|
||||
ret = felica_poller_process_error(error);
|
||||
break;
|
||||
}
|
||||
|
||||
bit_buffer_copy(rx_buffer, instance->rx_buffer);
|
||||
if(!felica_crc_check(instance->rx_buffer)) {
|
||||
ret = FelicaErrorWrongCrc;
|
||||
break;
|
||||
}
|
||||
|
||||
felica_crc_trim(rx_buffer);
|
||||
} while(false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FelicaError felica_poller_async_polling(
|
||||
FelicaPoller* instance,
|
||||
const FelicaPollerPollingCommand* cmd,
|
||||
FelicaPollerPollingResponse* resp) {
|
||||
furi_assert(instance);
|
||||
furi_assert(cmd);
|
||||
furi_assert(resp);
|
||||
|
||||
FelicaError error = FelicaErrorNone;
|
||||
|
||||
do {
|
||||
bit_buffer_set_size_bytes(instance->tx_buffer, 2);
|
||||
// Set frame len
|
||||
bit_buffer_set_byte(
|
||||
instance->tx_buffer, 0, sizeof(FelicaPollerPollingCommand) + FELICA_CRC_SIZE);
|
||||
// Set command code
|
||||
bit_buffer_set_byte(instance->tx_buffer, 1, FELICA_POLLER_CMD_POLLING_REQ_CODE);
|
||||
// Set other data
|
||||
bit_buffer_append_bytes(
|
||||
instance->tx_buffer, (uint8_t*)cmd, sizeof(FelicaPollerPollingCommand));
|
||||
|
||||
error = felica_poller_frame_exchange(
|
||||
instance, instance->tx_buffer, instance->rx_buffer, FELICA_POLLER_POLLING_FWT);
|
||||
|
||||
if(error != FelicaErrorNone) break;
|
||||
if(bit_buffer_get_byte(instance->rx_buffer, 1) != FELICA_POLLER_CMD_POLLING_RESP_CODE) {
|
||||
error = FelicaErrorProtocol;
|
||||
break;
|
||||
}
|
||||
if(bit_buffer_get_size_bytes(instance->rx_buffer) <
|
||||
sizeof(FelicaIDm) + sizeof(FelicaPMm) + 1) {
|
||||
error = FelicaErrorProtocol;
|
||||
break;
|
||||
}
|
||||
|
||||
bit_buffer_write_bytes_mid(instance->rx_buffer, resp->idm.data, 2, sizeof(FelicaIDm));
|
||||
bit_buffer_write_bytes_mid(
|
||||
instance->rx_buffer, resp->pmm.data, sizeof(FelicaIDm) + 2, sizeof(FelicaPMm));
|
||||
|
||||
} while(false);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
FelicaError felica_poller_async_activate(FelicaPoller* instance, FelicaData* data) {
|
||||
furi_assert(instance);
|
||||
|
||||
felica_reset(data);
|
||||
|
||||
FelicaError ret;
|
||||
|
||||
do {
|
||||
bit_buffer_reset(instance->tx_buffer);
|
||||
bit_buffer_reset(instance->rx_buffer);
|
||||
|
||||
// Send Polling command
|
||||
const FelicaPollerPollingCommand polling_cmd = {
|
||||
.system_code = FELICA_SYSTEM_CODE_CODE,
|
||||
.request_code = 0,
|
||||
.time_slot = FELICA_TIME_SLOT_1,
|
||||
};
|
||||
FelicaPollerPollingResponse polling_resp = {};
|
||||
|
||||
ret = felica_poller_async_polling(instance, &polling_cmd, &polling_resp);
|
||||
|
||||
if(ret != FelicaErrorNone) {
|
||||
FURI_LOG_T(TAG, "Activation failed error: %d", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
data->idm = polling_resp.idm;
|
||||
data->pmm = polling_resp.pmm;
|
||||
instance->state = FelicaPollerStateActivated;
|
||||
} while(false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "felica_poller.h"
|
||||
|
||||
#include <toolbox/bit_buffer.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define FELICA_POLLER_MAX_BUFFER_SIZE (256U)
|
||||
|
||||
#define FELICA_POLLER_POLLING_FWT (200000U)
|
||||
|
||||
#define FELICA_POLLER_CMD_POLLING_REQ_CODE (0x00U)
|
||||
#define FELICA_POLLER_CMD_POLLING_RESP_CODE (0x01U)
|
||||
|
||||
typedef enum {
|
||||
FelicaPollerStateIdle,
|
||||
FelicaPollerStateActivated,
|
||||
} FelicaPollerState;
|
||||
|
||||
struct FelicaPoller {
|
||||
Nfc* nfc;
|
||||
FelicaPollerState state;
|
||||
FelicaData* data;
|
||||
BitBuffer* tx_buffer;
|
||||
BitBuffer* rx_buffer;
|
||||
|
||||
NfcGenericEvent general_event;
|
||||
FelicaPollerEvent felica_event;
|
||||
FelicaPollerEventData felica_event_data;
|
||||
NfcGenericCallback callback;
|
||||
void* context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint16_t system_code;
|
||||
uint8_t request_code;
|
||||
uint8_t time_slot;
|
||||
} FelicaPollerPollingCommand;
|
||||
|
||||
typedef struct {
|
||||
FelicaIDm idm;
|
||||
FelicaPMm pmm;
|
||||
uint8_t request_data[2];
|
||||
} FelicaPollerPollingResponse;
|
||||
|
||||
const FelicaData* felica_poller_get_data(FelicaPoller* instance);
|
||||
|
||||
FelicaError felica_poller_async_polling(
|
||||
FelicaPoller* instance,
|
||||
const FelicaPollerPollingCommand* cmd,
|
||||
FelicaPollerPollingResponse* resp);
|
||||
|
||||
FelicaError felica_poller_async_activate(FelicaPoller* instance, FelicaData* data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user