mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-26 07:18:06 +00:00
refactor: optimize EventParser with idiomatic Rust patterns
This commit is contained in:
@@ -224,7 +224,8 @@ impl EventParser {
|
|||||||
let adapter_callback = Arc::new(move |event: &DexEvent| {
|
let adapter_callback = Arc::new(move |event: &DexEvent| {
|
||||||
callback(event.clone());
|
callback(event.clone());
|
||||||
});
|
});
|
||||||
Self::parse_versioned_transaction(
|
let accounts = versioned_tx.message.static_account_keys();
|
||||||
|
Self::parse_instruction_events_from_versioned_transaction(
|
||||||
protocols,
|
protocols,
|
||||||
event_type_filter,
|
event_type_filter,
|
||||||
&versioned_tx,
|
&versioned_tx,
|
||||||
@@ -232,45 +233,13 @@ impl EventParser {
|
|||||||
slot,
|
slot,
|
||||||
block_time,
|
block_time,
|
||||||
recv_us,
|
recv_us,
|
||||||
|
accounts,
|
||||||
|
inner_instructions,
|
||||||
bot_wallet,
|
bot_wallet,
|
||||||
transaction_index,
|
transaction_index,
|
||||||
inner_instructions,
|
|
||||||
adapter_callback,
|
adapter_callback,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn parse_versioned_transaction(
|
|
||||||
protocols: &[Protocol],
|
|
||||||
event_type_filter: Option<&EventTypeFilter>,
|
|
||||||
versioned_tx: &VersionedTransaction,
|
|
||||||
signature: Signature,
|
|
||||||
slot: Option<u64>,
|
|
||||||
block_time: Option<Timestamp>,
|
|
||||||
recv_us: i64,
|
|
||||||
bot_wallet: Option<Pubkey>,
|
|
||||||
transaction_index: Option<u64>,
|
|
||||||
inner_instructions: &[InnerInstructions],
|
|
||||||
callback: Arc<dyn for<'a> Fn(&'a DexEvent) + Send + Sync>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let accounts: Vec<Pubkey> = versioned_tx.message.static_account_keys().to_vec();
|
|
||||||
Self::parse_instruction_events_from_versioned_transaction(
|
|
||||||
protocols,
|
|
||||||
event_type_filter,
|
|
||||||
versioned_tx,
|
|
||||||
signature,
|
|
||||||
slot,
|
|
||||||
block_time,
|
|
||||||
recv_us,
|
|
||||||
&accounts,
|
|
||||||
inner_instructions,
|
|
||||||
bot_wallet,
|
|
||||||
transaction_index,
|
|
||||||
callback,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_grpc_transaction_owned(
|
pub async fn parse_grpc_transaction_owned(
|
||||||
@@ -351,9 +320,6 @@ impl EventParser {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// 使用 Arc 包装共享数据,避免不必要的克隆
|
|
||||||
let accounts_arc = Arc::new(accounts);
|
|
||||||
let inner_instructions_arc = Arc::new(inner_instructions);
|
|
||||||
// 解析指令事件
|
// 解析指令事件
|
||||||
let instructions = &message.instructions;
|
let instructions = &message.instructions;
|
||||||
Self::parse_instruction_events_from_grpc_transaction(
|
Self::parse_instruction_events_from_grpc_transaction(
|
||||||
@@ -364,8 +330,8 @@ impl EventParser {
|
|||||||
slot,
|
slot,
|
||||||
block_time,
|
block_time,
|
||||||
recv_us,
|
recv_us,
|
||||||
&accounts_arc,
|
&accounts,
|
||||||
&inner_instructions_arc,
|
&inner_instructions,
|
||||||
bot_wallet,
|
bot_wallet,
|
||||||
transaction_index,
|
transaction_index,
|
||||||
callback.clone(),
|
callback.clone(),
|
||||||
@@ -462,9 +428,6 @@ impl EventParser {
|
|||||||
);
|
);
|
||||||
accounts.extend_from_slice(versioned_tx.message.static_account_keys());
|
accounts.extend_from_slice(versioned_tx.message.static_account_keys());
|
||||||
accounts.extend(address_table_lookups);
|
accounts.extend(address_table_lookups);
|
||||||
// 使用 Arc 包装共享数据,避免不必要的克隆
|
|
||||||
let accounts_arc = Arc::new(accounts);
|
|
||||||
let inner_instructions_arc = Arc::new(inner_instructions);
|
|
||||||
|
|
||||||
let slot = transaction.slot;
|
let slot = transaction.slot;
|
||||||
let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 });
|
let block_time = transaction.block_time.map(|t| Timestamp { seconds: t as i64, nanos: 0 });
|
||||||
@@ -480,15 +443,42 @@ impl EventParser {
|
|||||||
Some(slot),
|
Some(slot),
|
||||||
block_time,
|
block_time,
|
||||||
recv_us,
|
recv_us,
|
||||||
&accounts_arc,
|
&accounts,
|
||||||
&inner_instructions_arc,
|
inner_instructions,
|
||||||
bot_wallet,
|
bot_wallet,
|
||||||
transaction_index,
|
transaction_index,
|
||||||
callback.clone(),
|
callback.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
/// Helper function to create EventMetadata from common parameters
|
||||||
|
#[inline]
|
||||||
|
fn create_metadata(
|
||||||
|
config: &GenericEventParseConfig,
|
||||||
|
signature: Signature,
|
||||||
|
slot: u64,
|
||||||
|
block_time: Option<Timestamp>,
|
||||||
|
recv_us: i64,
|
||||||
|
outer_index: i64,
|
||||||
|
inner_index: Option<i64>,
|
||||||
|
transaction_index: Option<u64>,
|
||||||
|
) -> EventMetadata {
|
||||||
|
let timestamp = block_time.unwrap_or(Timestamp { seconds: 0, nanos: 0 });
|
||||||
|
let block_time_ms = timestamp.seconds * 1000 + (timestamp.nanos as i64) / 1_000_000;
|
||||||
|
EventMetadata::new(
|
||||||
|
signature,
|
||||||
|
slot,
|
||||||
|
timestamp.seconds,
|
||||||
|
block_time_ms,
|
||||||
|
config.protocol_type.clone(),
|
||||||
|
config.event_type.clone(),
|
||||||
|
config.program_id,
|
||||||
|
outer_index,
|
||||||
|
inner_index,
|
||||||
|
recv_us,
|
||||||
|
transaction_index,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通用的内联指令解析方法
|
/// 通用的内联指令解析方法
|
||||||
@@ -504,26 +494,19 @@ impl EventParser {
|
|||||||
inner_index: Option<i64>,
|
inner_index: Option<i64>,
|
||||||
transaction_index: Option<u64>,
|
transaction_index: Option<u64>,
|
||||||
) -> Option<DexEvent> {
|
) -> Option<DexEvent> {
|
||||||
if let Some(parser) = config.inner_instruction_parser {
|
config.inner_instruction_parser.and_then(|parser| {
|
||||||
let timestamp = block_time.unwrap_or(Timestamp { seconds: 0, nanos: 0 });
|
let metadata = Self::create_metadata(
|
||||||
let block_time_ms = timestamp.seconds * 1000 + (timestamp.nanos as i64) / 1_000_000;
|
config,
|
||||||
let metadata = EventMetadata::new(
|
|
||||||
signature,
|
signature,
|
||||||
slot,
|
slot,
|
||||||
timestamp.seconds,
|
block_time,
|
||||||
block_time_ms,
|
recv_us,
|
||||||
config.protocol_type.clone(),
|
|
||||||
config.event_type.clone(),
|
|
||||||
config.program_id,
|
|
||||||
outer_index,
|
outer_index,
|
||||||
inner_index,
|
inner_index,
|
||||||
recv_us,
|
|
||||||
transaction_index,
|
transaction_index,
|
||||||
);
|
);
|
||||||
parser(data, metadata)
|
parser(data, metadata)
|
||||||
} else {
|
})
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通用的指令解析方法
|
/// 通用的指令解析方法
|
||||||
@@ -540,26 +523,59 @@ impl EventParser {
|
|||||||
inner_index: Option<i64>,
|
inner_index: Option<i64>,
|
||||||
transaction_index: Option<u64>,
|
transaction_index: Option<u64>,
|
||||||
) -> Option<DexEvent> {
|
) -> Option<DexEvent> {
|
||||||
if let Some(parser) = config.instruction_parser {
|
config.instruction_parser.and_then(|parser| {
|
||||||
let timestamp = block_time.unwrap_or(Timestamp { seconds: 0, nanos: 0 });
|
let metadata = Self::create_metadata(
|
||||||
let block_time_ms = timestamp.seconds * 1000 + (timestamp.nanos as i64) / 1_000_000;
|
config,
|
||||||
let metadata = EventMetadata::new(
|
|
||||||
signature,
|
signature,
|
||||||
slot,
|
slot,
|
||||||
timestamp.seconds,
|
block_time,
|
||||||
block_time_ms,
|
recv_us,
|
||||||
config.protocol_type.clone(),
|
|
||||||
config.event_type.clone(),
|
|
||||||
config.program_id,
|
|
||||||
outer_index,
|
outer_index,
|
||||||
inner_index,
|
inner_index,
|
||||||
recv_us,
|
|
||||||
transaction_index,
|
transaction_index,
|
||||||
);
|
);
|
||||||
parser(data, account_pubkeys, metadata)
|
parser(data, account_pubkeys, metadata)
|
||||||
} else {
|
})
|
||||||
None
|
}
|
||||||
|
|
||||||
|
/// 从内联指令中解析事件数据 - 通用实现
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn parse_events_from_inner_instruction_data(
|
||||||
|
data: &[u8],
|
||||||
|
signature: Signature,
|
||||||
|
slot: u64,
|
||||||
|
block_time: Option<Timestamp>,
|
||||||
|
recv_us: i64,
|
||||||
|
outer_index: i64,
|
||||||
|
inner_index: Option<i64>,
|
||||||
|
transaction_index: Option<u64>,
|
||||||
|
config: &GenericEventParseConfig,
|
||||||
|
) -> Vec<DexEvent> {
|
||||||
|
// Use SIMD-optimized data validation with correct discriminator length
|
||||||
|
let discriminator_len = config.inner_instruction_discriminator.len();
|
||||||
|
if !SimdUtils::validate_instruction_data_simd(data, 16, discriminator_len) {
|
||||||
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use SIMD-optimized discriminator matching
|
||||||
|
if !SimdUtils::fast_discriminator_match(data, config.inner_instruction_discriminator) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = &data[16..];
|
||||||
|
Self::parse_inner_instruction_event(
|
||||||
|
config,
|
||||||
|
data,
|
||||||
|
signature,
|
||||||
|
slot,
|
||||||
|
block_time,
|
||||||
|
recv_us,
|
||||||
|
outer_index,
|
||||||
|
inner_index,
|
||||||
|
transaction_index,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从内联指令中解析事件数据
|
/// 从内联指令中解析事件数据
|
||||||
@@ -575,29 +591,8 @@ impl EventParser {
|
|||||||
transaction_index: Option<u64>,
|
transaction_index: Option<u64>,
|
||||||
config: &GenericEventParseConfig,
|
config: &GenericEventParseConfig,
|
||||||
) -> Vec<DexEvent> {
|
) -> Vec<DexEvent> {
|
||||||
// Use SIMD-optimized data validation with correct discriminator length
|
Self::parse_events_from_inner_instruction_data(
|
||||||
let discriminator_len = config.inner_instruction_discriminator.len();
|
|
||||||
if !SimdUtils::validate_instruction_data_simd(
|
|
||||||
&inner_instruction.data,
|
&inner_instruction.data,
|
||||||
16,
|
|
||||||
discriminator_len,
|
|
||||||
) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use SIMD-optimized discriminator matching
|
|
||||||
if !SimdUtils::fast_discriminator_match(
|
|
||||||
&inner_instruction.data,
|
|
||||||
config.inner_instruction_discriminator,
|
|
||||||
) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = &inner_instruction.data[16..];
|
|
||||||
let mut events = Vec::new();
|
|
||||||
if let Some(event) = Self::parse_inner_instruction_event(
|
|
||||||
config,
|
|
||||||
data,
|
|
||||||
signature,
|
signature,
|
||||||
slot,
|
slot,
|
||||||
block_time,
|
block_time,
|
||||||
@@ -605,10 +600,8 @@ impl EventParser {
|
|||||||
outer_index,
|
outer_index,
|
||||||
inner_index,
|
inner_index,
|
||||||
transaction_index,
|
transaction_index,
|
||||||
) {
|
config,
|
||||||
events.push(event);
|
)
|
||||||
}
|
|
||||||
events
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从内联指令中解析事件数据
|
/// 从内联指令中解析事件数据
|
||||||
@@ -624,29 +617,8 @@ impl EventParser {
|
|||||||
transaction_index: Option<u64>,
|
transaction_index: Option<u64>,
|
||||||
config: &GenericEventParseConfig,
|
config: &GenericEventParseConfig,
|
||||||
) -> Vec<DexEvent> {
|
) -> Vec<DexEvent> {
|
||||||
// Use SIMD-optimized data validation with correct discriminator length
|
Self::parse_events_from_inner_instruction_data(
|
||||||
let discriminator_len = config.inner_instruction_discriminator.len();
|
|
||||||
if !SimdUtils::validate_instruction_data_simd(
|
|
||||||
&inner_instruction.data,
|
&inner_instruction.data,
|
||||||
16,
|
|
||||||
discriminator_len,
|
|
||||||
) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use SIMD-optimized discriminator matching
|
|
||||||
if !SimdUtils::fast_discriminator_match(
|
|
||||||
&inner_instruction.data,
|
|
||||||
config.inner_instruction_discriminator,
|
|
||||||
) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = &inner_instruction.data[16..];
|
|
||||||
let mut events = Vec::new();
|
|
||||||
if let Some(event) = Self::parse_inner_instruction_event(
|
|
||||||
config,
|
|
||||||
data,
|
|
||||||
signature,
|
signature,
|
||||||
slot,
|
slot,
|
||||||
block_time,
|
block_time,
|
||||||
@@ -654,10 +626,8 @@ impl EventParser {
|
|||||||
outer_index,
|
outer_index,
|
||||||
inner_index,
|
inner_index,
|
||||||
transaction_index,
|
transaction_index,
|
||||||
) {
|
config,
|
||||||
events.push(event);
|
)
|
||||||
}
|
|
||||||
events
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从指令中解析事件
|
/// 从指令中解析事件
|
||||||
@@ -736,9 +706,7 @@ impl EventParser {
|
|||||||
for (_disc, config, mut event) in all_results {
|
for (_disc, config, mut event) in all_results {
|
||||||
// 阻塞处理:原有的同步逻辑
|
// 阻塞处理:原有的同步逻辑
|
||||||
let mut inner_instruction_event: Option<DexEvent> = None;
|
let mut inner_instruction_event: Option<DexEvent> = None;
|
||||||
if inner_instructions.is_some() {
|
if let Some(inner_instructions_ref) = inner_instructions {
|
||||||
let inner_instructions_ref = inner_instructions.unwrap();
|
|
||||||
|
|
||||||
// 并行执行两个任务
|
// 并行执行两个任务
|
||||||
let (inner_event_result, swap_data_result) = std::thread::scope(|s| {
|
let (inner_event_result, swap_data_result) = std::thread::scope(|s| {
|
||||||
let inner_event_handle = s.spawn(|| {
|
let inner_event_handle = s.spawn(|| {
|
||||||
@@ -762,7 +730,7 @@ impl EventParser {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let swap_data_handle = s.spawn(|| {
|
let swap_data_handle = s.spawn(|| {
|
||||||
if !event.metadata().swap_data.is_some() {
|
if event.metadata().swap_data.is_none() {
|
||||||
parse_swap_data_from_next_instructions(
|
parse_swap_data_from_next_instructions(
|
||||||
&event,
|
&event,
|
||||||
inner_instructions_ref,
|
inner_instructions_ref,
|
||||||
@@ -878,9 +846,7 @@ impl EventParser {
|
|||||||
for (_disc, config, mut event) in all_results {
|
for (_disc, config, mut event) in all_results {
|
||||||
// 阻塞处理:原有的同步逻辑
|
// 阻塞处理:原有的同步逻辑
|
||||||
let mut inner_instruction_event: Option<DexEvent> = None;
|
let mut inner_instruction_event: Option<DexEvent> = None;
|
||||||
if inner_instructions.is_some() {
|
if let Some(inner_instructions_ref) = inner_instructions {
|
||||||
let inner_instructions_ref = inner_instructions.unwrap();
|
|
||||||
|
|
||||||
// 并行执行两个任务
|
// 并行执行两个任务
|
||||||
let (inner_event_result, swap_data_result) = std::thread::scope(|s| {
|
let (inner_event_result, swap_data_result) = std::thread::scope(|s| {
|
||||||
let inner_event_handle = s.spawn(|| {
|
let inner_event_handle = s.spawn(|| {
|
||||||
@@ -904,7 +870,7 @@ impl EventParser {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let swap_data_handle = s.spawn(|| {
|
let swap_data_handle = s.spawn(|| {
|
||||||
if !event.metadata().swap_data.is_some() {
|
if event.metadata().swap_data.is_none() {
|
||||||
parse_swap_data_from_next_grpc_instructions(
|
parse_swap_data_from_next_grpc_instructions(
|
||||||
&event,
|
&event,
|
||||||
inner_instructions_ref,
|
inner_instructions_ref,
|
||||||
@@ -963,24 +929,18 @@ impl EventParser {
|
|||||||
DexEvent::PumpFunCreateTokenEvent(token_info)
|
DexEvent::PumpFunCreateTokenEvent(token_info)
|
||||||
}
|
}
|
||||||
DexEvent::PumpFunTradeEvent(mut trade_info) => {
|
DexEvent::PumpFunTradeEvent(mut trade_info) => {
|
||||||
if is_dev_address_in_signature(&signature, &trade_info.user)
|
trade_info.is_dev_create_token_trade =
|
||||||
|| is_dev_address_in_signature(&signature, &trade_info.creator)
|
is_dev_address_in_signature(&signature, &trade_info.user)
|
||||||
{
|
|| is_dev_address_in_signature(&signature, &trade_info.creator);
|
||||||
trade_info.is_dev_create_token_trade = true;
|
trade_info.is_bot = Some(trade_info.user) == bot_wallet;
|
||||||
} else if Some(trade_info.user) == bot_wallet {
|
|
||||||
trade_info.is_bot = true;
|
if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() {
|
||||||
} else {
|
swap_data.from_amount = if trade_info.is_buy {
|
||||||
trade_info.is_dev_create_token_trade = false;
|
trade_info.sol_amount
|
||||||
}
|
} else {
|
||||||
if trade_info.metadata.swap_data.is_some() {
|
trade_info.token_amount
|
||||||
trade_info.metadata.swap_data.as_mut().unwrap().from_amount =
|
};
|
||||||
if trade_info.is_buy {
|
swap_data.to_amount = if trade_info.is_buy {
|
||||||
trade_info.sol_amount
|
|
||||||
} else {
|
|
||||||
trade_info.token_amount
|
|
||||||
};
|
|
||||||
trade_info.metadata.swap_data.as_mut().unwrap().to_amount = if trade_info.is_buy
|
|
||||||
{
|
|
||||||
trade_info.token_amount
|
trade_info.token_amount
|
||||||
} else {
|
} else {
|
||||||
trade_info.sol_amount
|
trade_info.sol_amount
|
||||||
@@ -989,20 +949,16 @@ impl EventParser {
|
|||||||
DexEvent::PumpFunTradeEvent(trade_info)
|
DexEvent::PumpFunTradeEvent(trade_info)
|
||||||
}
|
}
|
||||||
DexEvent::PumpSwapBuyEvent(mut trade_info) => {
|
DexEvent::PumpSwapBuyEvent(mut trade_info) => {
|
||||||
if trade_info.metadata.swap_data.is_some() {
|
if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() {
|
||||||
trade_info.metadata.swap_data.as_mut().unwrap().from_amount =
|
swap_data.from_amount = trade_info.user_quote_amount_in;
|
||||||
trade_info.user_quote_amount_in;
|
swap_data.to_amount = trade_info.base_amount_out;
|
||||||
trade_info.metadata.swap_data.as_mut().unwrap().to_amount =
|
|
||||||
trade_info.base_amount_out;
|
|
||||||
}
|
}
|
||||||
DexEvent::PumpSwapBuyEvent(trade_info)
|
DexEvent::PumpSwapBuyEvent(trade_info)
|
||||||
}
|
}
|
||||||
DexEvent::PumpSwapSellEvent(mut trade_info) => {
|
DexEvent::PumpSwapSellEvent(mut trade_info) => {
|
||||||
if trade_info.metadata.swap_data.is_some() {
|
if let Some(swap_data) = trade_info.metadata.swap_data.as_mut() {
|
||||||
trade_info.metadata.swap_data.as_mut().unwrap().from_amount =
|
swap_data.from_amount = trade_info.base_amount_in;
|
||||||
trade_info.base_amount_in;
|
swap_data.to_amount = trade_info.user_quote_amount_out;
|
||||||
trade_info.metadata.swap_data.as_mut().unwrap().to_amount =
|
|
||||||
trade_info.user_quote_amount_out;
|
|
||||||
}
|
}
|
||||||
DexEvent::PumpSwapSellEvent(trade_info)
|
DexEvent::PumpSwapSellEvent(trade_info)
|
||||||
}
|
}
|
||||||
@@ -1011,13 +967,9 @@ impl EventParser {
|
|||||||
DexEvent::BonkPoolCreateEvent(pool_info)
|
DexEvent::BonkPoolCreateEvent(pool_info)
|
||||||
}
|
}
|
||||||
DexEvent::BonkTradeEvent(mut trade_info) => {
|
DexEvent::BonkTradeEvent(mut trade_info) => {
|
||||||
if is_bonk_dev_address_in_signature(&signature, &trade_info.payer) {
|
trade_info.is_dev_create_token_trade =
|
||||||
trade_info.is_dev_create_token_trade = true;
|
is_bonk_dev_address_in_signature(&signature, &trade_info.payer);
|
||||||
} else if Some(trade_info.payer) == bot_wallet {
|
trade_info.is_bot = Some(trade_info.payer) == bot_wallet;
|
||||||
trade_info.is_bot = true;
|
|
||||||
} else {
|
|
||||||
trade_info.is_dev_create_token_trade = false;
|
|
||||||
}
|
|
||||||
DexEvent::BonkTradeEvent(trade_info)
|
DexEvent::BonkTradeEvent(trade_info)
|
||||||
}
|
}
|
||||||
_ => event,
|
_ => event,
|
||||||
|
|||||||
Reference in New Issue
Block a user