working identity > can_user_run()
This commit is contained in:
parent
5f7ea5d2a7
commit
e63133aee6
4 changed files with 224 additions and 39 deletions
|
@ -22,7 +22,7 @@ use crate::core::ratelimiter;
|
||||||
|
|
||||||
use crate::core::botmodules;
|
use crate::core::botmodules;
|
||||||
use crate::core::botmodules::ModulesManager;
|
use crate::core::botmodules::ModulesManager;
|
||||||
use crate::core::identity::IdentityManager;
|
use crate::core::identity::{IdentityManager,Permissible};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||||
|
@ -222,7 +222,7 @@ impl BotInstance
|
||||||
|
|
||||||
while let Some(message) = self.incoming_messages.recv().await {
|
while let Some(message) = self.incoming_messages.recv().await {
|
||||||
// Below can be used to debug if I want to capture all messages
|
// Below can be used to debug if I want to capture all messages
|
||||||
println!("Received message: {:?}", message);
|
// println!("Received message: {:?}", message);
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
ServerMessage::Notice(msg) => {
|
ServerMessage::Notice(msg) => {
|
||||||
|
@ -271,7 +271,8 @@ impl BotInstance
|
||||||
// PRIVATE FUNCTIONS
|
// PRIVATE FUNCTIONS
|
||||||
|
|
||||||
|
|
||||||
async fn listener_main_prvmsg(&mut self,msg:PrivmsgMessage) -> () {
|
// async fn listener_main_prvmsg(&mut self,msg:PrivmsgMessage) -> () {
|
||||||
|
async fn listener_main_prvmsg(&self,msg:PrivmsgMessage) -> () {
|
||||||
|
|
||||||
// println!("(#{}) {}: {}", msg.channel_login, msg.sender.name, msg.message_text);
|
// println!("(#{}) {}: {}", msg.channel_login, msg.sender.name, msg.message_text);
|
||||||
|
|
||||||
|
@ -286,6 +287,11 @@ impl BotInstance
|
||||||
/*
|
/*
|
||||||
BotCommand handling -
|
BotCommand handling -
|
||||||
- [x] Checks if the input message is a prefix with command name or alias
|
- [x] Checks if the input message is a prefix with command name or alias
|
||||||
|
- [ ] Validate User can run based on identityModule(From_Bot)::can_user_run(
|
||||||
|
_usr:String,
|
||||||
|
_channelname:ChType,
|
||||||
|
_chat_badge:ChatBadge,
|
||||||
|
_cmdreqroles:Vec<UserRole>)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let inpt = msg.message_text.split("\n").next().expect("ERROR during BotCommand");
|
let inpt = msg.message_text.split("\n").next().expect("ERROR during BotCommand");
|
||||||
|
@ -293,21 +299,41 @@ impl BotInstance
|
||||||
// [x] Check if a bot command based on ...
|
// [x] Check if a bot command based on ...
|
||||||
// [x] prefix + command
|
// [x] prefix + command
|
||||||
|
|
||||||
let mut exec_bot_command = false;
|
let mut confirmed_bot_command = false;
|
||||||
if inpt == self.prefix.to_string() + c.command.as_str() {
|
if inpt == self.prefix.to_string() + c.command.as_str() {
|
||||||
exec_bot_command = true;
|
confirmed_bot_command = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [x] prefix + alias
|
// [x] prefix + alias
|
||||||
for alias in &c.alias {
|
for alias in &c.alias {
|
||||||
if inpt == self.prefix.to_string() + alias.as_str() {
|
if inpt == self.prefix.to_string() + alias.as_str() {
|
||||||
exec_bot_command = true;
|
confirmed_bot_command = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if exec_bot_command {
|
if confirmed_bot_command {
|
||||||
|
|
||||||
|
// self.identity.clone().can_user_run_PRVMSG(&msg, c.required_roles.clone());
|
||||||
|
|
||||||
|
// [ ] Around here, validate if permissable before executing
|
||||||
|
// match self.identity.clone().can_user_run_PRVMSG(&msg, c.required_roles.clone()) {
|
||||||
|
// Ok(Permissible::Allow) => c.execute(self.chat.clone(), msg.clone()).await,
|
||||||
|
// Ok(Permissible::Block) => println!("User Not allowed to run command"),
|
||||||
|
// _ => (),
|
||||||
|
// }
|
||||||
|
|
||||||
|
match self.identity.to_owned().can_user_run_PRVMSG(&msg, c.required_roles.clone()) {
|
||||||
|
// Ok(Permissible::Allow) => (),
|
||||||
|
Permissible::Allow => {
|
||||||
|
println!("Executed as permissible");
|
||||||
c.execute(self.chat.clone(), msg.clone()).await;
|
c.execute(self.chat.clone(), msg.clone()).await;
|
||||||
}
|
}
|
||||||
|
Permissible::Block => println!("User Not allowed to run command"),
|
||||||
|
// _ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// c.execute(self.chat.clone(), msg.clone()).await;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
crate::core::botmodules::BotAction::L(l) => l.execute(self.chat.clone(), msg.clone()).await,
|
crate::core::botmodules::BotAction::L(l) => l.execute(self.chat.clone(), msg.clone()).await,
|
||||||
|
|
|
@ -34,7 +34,7 @@ pub enum ModType {
|
||||||
|
|
||||||
pub use ModType::BotModule;
|
pub use ModType::BotModule;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||||
pub enum ChType {
|
pub enum ChType {
|
||||||
Channel(String),
|
Channel(String),
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::core::botmodules::{ModulesManager,Listener,BotModule,BotActionTrait,
|
||||||
use crate::core::botmodules::bot_actions::actions_util;
|
use crate::core::botmodules::bot_actions::actions_util;
|
||||||
|
|
||||||
use crate::core::botinstance::{self};
|
use crate::core::botinstance::{self};
|
||||||
use twitch_irc::message::PrivmsgMessage;
|
use twitch_irc::message::{Badge, PrivmsgMessage};
|
||||||
|
|
||||||
use crate::core::botmodules::ChType;
|
use crate::core::botmodules::ChType;
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ pub fn init(mgr:&mut ModulesManager)
|
||||||
|
|
||||||
async fn cmd_promote(mut _chat:botinstance::Chat,_msg:PrivmsgMessage) {
|
async fn cmd_promote(mut _chat:botinstance::Chat,_msg:PrivmsgMessage) {
|
||||||
//println!("(#{}) {}: {}", msg.channel_login, msg.sender.name, msg.message_text);
|
//println!("(#{}) {}: {}", msg.channel_login, msg.sender.name, msg.message_text);
|
||||||
|
println!("Called cmd promote");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,13 +44,19 @@ pub fn init(mgr:&mut ModulesManager)
|
||||||
|
|
||||||
|
|
||||||
async fn cmd_demote(mut _chat:botinstance::Chat,_msg:PrivmsgMessage) {
|
async fn cmd_demote(mut _chat:botinstance::Chat,_msg:PrivmsgMessage) {
|
||||||
|
println!("Called cmd demote");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// #[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||||
|
// pub enum ChType {
|
||||||
|
// Channel(String),
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq , Clone)]
|
||||||
pub enum UserRole {
|
pub enum UserRole {
|
||||||
Chatter,
|
Chatter,
|
||||||
Mod(ChType), // String specifies Channel
|
Mod(ChType), // String specifies Channel
|
||||||
|
@ -61,16 +67,17 @@ pub enum UserRole {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum Permissible {
|
pub enum Permissible {
|
||||||
Allow,
|
Allow,
|
||||||
Block
|
Block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct IdentityManager {
|
pub struct IdentityManager {
|
||||||
special_roles_users : HashMap<String,Vec<UserRole>>,
|
special_roles_users : HashMap<String,Vec<UserRole>>, // # <-- (!) This must be String instead of ChType because we're checking a User not a Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ChatBadge {
|
pub enum ChatBadge {
|
||||||
Broadcaster,
|
Broadcaster,
|
||||||
Mod,
|
Mod,
|
||||||
}
|
}
|
||||||
|
@ -89,19 +96,57 @@ impl IdentityManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn canUserRun(self,
|
// [ ] Maybe I should create a can_user_run version that simply takes PrvMsg, but then calls can_user_run directly
|
||||||
|
|
||||||
|
// pub fn can_user_run_PRVMSG(self,msg:&PrivmsgMessage,cmdreqroles:Vec<UserRole>) -> Result<Permissible,Box<dyn Error>>
|
||||||
|
pub fn can_user_run_PRVMSG(self,msg:&PrivmsgMessage,cmdreqroles:Vec<UserRole>) -> Permissible
|
||||||
|
{
|
||||||
|
// println!("(#{}) {}: {}", msg.channel_login, msg.sender.name, msg.message_text);
|
||||||
|
|
||||||
|
// [ ] Check what Badges in PrivmsgMessage
|
||||||
|
|
||||||
|
let mut sender_badge:Option<ChatBadge> = None;
|
||||||
|
|
||||||
|
for b in &msg.badges {
|
||||||
|
if b.name == "moderator" {
|
||||||
|
sender_badge = Some(ChatBadge::Mod);
|
||||||
|
} else if b.name == "broadcaster" {
|
||||||
|
sender_badge = Some(ChatBadge::Broadcaster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if &msg.badges.contains(Badge{}) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
if let Some(sender_badge) = sender_badge {
|
||||||
|
return self.can_user_run(msg.sender.name.to_owned(),
|
||||||
|
ChType::Channel(msg.channel_login.to_owned()),
|
||||||
|
sender_badge,
|
||||||
|
cmdreqroles
|
||||||
|
) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// [ ] Call can_user_run()
|
||||||
|
Permissible::Block
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_user_run(mut self,
|
||||||
usr:String,
|
usr:String,
|
||||||
channelname:ChType,
|
channelname:ChType,
|
||||||
chatBadge:ChatBadge,
|
chat_badge:ChatBadge,
|
||||||
cmdreqroles:Vec<UserRole>
|
cmdreqroles:Vec<UserRole>
|
||||||
) -> Result<Permissible,Box<dyn Error>> {
|
// ) -> Result<Permissible,Box<dyn Error>> {
|
||||||
|
) -> Permissible {
|
||||||
/*
|
/*
|
||||||
canUserRun -
|
canUserRun -
|
||||||
|
|
||||||
Input :
|
Input :
|
||||||
usr:String,
|
usr:String,
|
||||||
channelname:ChType,
|
channelname:ChType,
|
||||||
chatBadge:ChatBadge,
|
chat_badge:ChatBadge,
|
||||||
cmdreqroles:Vec<UserRole>
|
cmdreqroles:Vec<UserRole>
|
||||||
|
|
||||||
Output : Result<Permissible,Box<dyn Error>>
|
Output : Result<Permissible,Box<dyn Error>>
|
||||||
|
@ -118,18 +163,125 @@ impl IdentityManager {
|
||||||
|
|
||||||
// Requirements
|
// Requirements
|
||||||
/*
|
/*
|
||||||
[ ] If cmdreqroles is empty vector , automatically assume Ok(Permissible::Allow)
|
[x] If cmdreqroles is empty vector , automatically assume Ok(Permissible::Allow)
|
||||||
[ ] If chatBadge::Broadcaster ...
|
[x] If chatBadge::Broadcaster ...
|
||||||
[ ] and cmdreqroles includes UserRole::Broadcaster , Ok(Permissible::Allow)
|
[x] and cmdreqroles includes UserRole::Broadcaster , Ok(Permissible::Allow)
|
||||||
[ ] and cmdreqroles includes UserRole::Mod("") OR UserRole::SupMod("") , Ok(Permissible::Allow)
|
[x] and cmdreqroles includes UserRole::Mod("") OR UserRole::SupMod("") , Ok(Permissible::Allow)
|
||||||
[ ] If cmdreqroles includes UserRole::Mod("") , checks if chatter has UserRole::Mod(channelname::ChType) to determine if Ok(Permissible::Allow)
|
[x] If chatBadge::Mod ...
|
||||||
[ ] If cmdreqroles includes UserRole::SupMod("") , checks if chatter has UserRole::SupMod(channelname::ChType) to determine if Ok(Permissible::Allow)
|
[x] Check if they have either UserRole::Mod(channelname::ChType) or UserRole::SupMod(channelname::ChType)
|
||||||
[ ] If cmdreqroles includes UserRole::BotAdmin and chatter has UserRole::BotAdmin , Ok(Permissible::Allow)
|
[x] If not, assign them UserRole::Mod(channelname::ChType)
|
||||||
[ ] Otherwise, Ok(Permissible::Block)
|
[x] If cmdreqroles includes UserRole::Mod("") , checks if chatter has UserRole::Mod(channelname::ChType) or UserRole::SupMod(channelname::ChType) to determine if Ok(Permissible::Allow)
|
||||||
|
[x] If cmdreqroles includes UserRole::SupMod("") , checks if chatter has UserRole::SupMod(channelname::ChType) to determine if Ok(Permissible::Allow)
|
||||||
|
[x] If cmdreqroles includes UserRole::BotAdmin and chatter has UserRole::BotAdmin , Ok(Permissible::Allow)
|
||||||
|
[x] Otherwise, Ok(Permissible::Block)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// [x] If cmdreqroles is empty vector , automatically assume Ok(Permissible::Allow)
|
||||||
|
|
||||||
Ok(Permissible::Allow)
|
if cmdreqroles.len() == 0 {
|
||||||
|
// return Ok(Permissible::Allow)
|
||||||
|
return Permissible::Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
match chat_badge {
|
||||||
|
|
||||||
|
// [x] If chatBadge::Broadcaster ...
|
||||||
|
// [x] and cmdreqroles includes UserRole::Broadcaster , Ok(Permissible::Allow)
|
||||||
|
// [x] and cmdreqroles includes UserRole::Mod("") OR UserRole::SupMod("") , Ok(Permissible::Allow)
|
||||||
|
|
||||||
|
ChatBadge::Broadcaster => {
|
||||||
|
if cmdreqroles.contains(&UserRole::Broadcaster) ||
|
||||||
|
cmdreqroles.contains(&UserRole::Mod(ChType::Channel(String::new()))) ||
|
||||||
|
cmdreqroles.contains(&UserRole::SupMod(ChType::Channel(String::new()))) {
|
||||||
|
// return Ok(Permissible::Allow)
|
||||||
|
return Permissible::Allow
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// [x] If chatBadge::Mod ...
|
||||||
|
// [x] Check if they have either UserRole::Mod(channelname::ChType) or UserRole::SupMod(channelname::ChType)
|
||||||
|
// [x] If not, assign them UserRole::Mod(channelname::ChType)
|
||||||
|
|
||||||
|
ChatBadge::Mod => {
|
||||||
|
|
||||||
|
println!("Mod Chatbadge detected");
|
||||||
|
|
||||||
|
println!("debug special roles : {:?}",self.special_roles_users);
|
||||||
|
println!("debug usr : {}",&usr.to_lowercase());
|
||||||
|
|
||||||
|
// let Some((k,v)) = self.special_roles_users.get_key_value(usr);
|
||||||
|
match self.special_roles_users.get_mut(&usr.to_lowercase()) {
|
||||||
|
Some(usrroles) => {
|
||||||
|
println!("contains mod : {}", usrroles.contains(&UserRole::Mod(channelname.clone())));
|
||||||
|
println!("contains supmod : {}", usrroles.contains(&UserRole::SupMod(channelname.clone())));
|
||||||
|
if usrroles.contains(&UserRole::Mod(channelname.clone())) ||
|
||||||
|
usrroles.contains(&UserRole::SupMod(channelname.clone())) {
|
||||||
|
// Do nothing - this is expected
|
||||||
|
} else {
|
||||||
|
// in this case, they have a ChatBadge::Mod but should have this for the channel
|
||||||
|
usrroles.push(UserRole::Mod(channelname.clone()));
|
||||||
|
println!("debug special roles : {:?}",self.special_roles_users);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
// _ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// [x] If cmdreqroles includes UserRole::Mod("") , checks if chatter has UserRole::Mod(channelname::ChType) or UserRole::SupMod(channelname::ChType) to determine if Ok(Permissible::Allow)
|
||||||
|
|
||||||
|
println!("cmd required roles : {:?}",cmdreqroles);
|
||||||
|
|
||||||
|
if cmdreqroles.contains(&UserRole::Mod(ChType::Channel(String::new()))) {
|
||||||
|
// match self.special_roles_users.get(&channelname) {
|
||||||
|
// Some(usrroles) => {},
|
||||||
|
// None => (),
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
println!("Mod Role required");
|
||||||
|
|
||||||
|
if let Some(a) = self.special_roles_users.get(&usr.to_lowercase()) {
|
||||||
|
if a.contains(&UserRole::Mod(channelname.clone())) || a.contains(&UserRole::SupMod(channelname.clone())){
|
||||||
|
// return Ok(Permissible::Allow);
|
||||||
|
return Permissible::Allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [x] If cmdreqroles includes UserRole::SupMod("") , checks if chatter has UserRole::SupMod(channelname::ChType) to determine if Ok(Permissible::Allow)
|
||||||
|
|
||||||
|
|
||||||
|
if cmdreqroles.contains(&UserRole::SupMod(ChType::Channel(String::new()))) {
|
||||||
|
if let Some(a) = self.special_roles_users.get(&usr.to_lowercase()) {
|
||||||
|
if a.contains(&UserRole::SupMod(channelname.clone())) {
|
||||||
|
// return Ok(Permissible::Allow);
|
||||||
|
return Permissible::Allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [x] If cmdreqroles includes UserRole::BotAdmin and chatter has UserRole::BotAdmin , Ok(Permissible::Allow)
|
||||||
|
|
||||||
|
println!("Eval cmdreqroles with botadmin : {}",cmdreqroles.contains(&UserRole::BotAdmin));
|
||||||
|
|
||||||
|
if cmdreqroles.contains(&UserRole::BotAdmin) {
|
||||||
|
println!("special roles get : {:?}",self.special_roles_users.get(&usr.to_lowercase()));
|
||||||
|
if let Some(a) = self.special_roles_users.get(&usr.to_lowercase()) {
|
||||||
|
println!("special roles contains BotAdmin: {}",a.contains(&UserRole::BotAdmin));
|
||||||
|
if a.contains(&UserRole::BotAdmin) {
|
||||||
|
// return Ok(Permissible::Allow);
|
||||||
|
return Permissible::Allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Permissible::Block
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,14 @@
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
use crate::core::botmodules::{ModulesManager,Listener,BotModule,BotActionTrait, BotCommand};
|
use crate::core::botmodules::{ModulesManager,Listener,BotModule,BotActionTrait, BotCommand,ChType};
|
||||||
use crate::core::botmodules::bot_actions::actions_util;
|
use crate::core::botmodules::bot_actions::actions_util;
|
||||||
|
|
||||||
use crate::core::botinstance::{self};
|
use crate::core::botinstance::{self};
|
||||||
use twitch_irc::message::PrivmsgMessage;
|
use twitch_irc::message::PrivmsgMessage;
|
||||||
|
|
||||||
|
use crate::core::identity;
|
||||||
|
|
||||||
pub fn init(mgr:&mut ModulesManager)
|
pub fn init(mgr:&mut ModulesManager)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -30,7 +32,10 @@ pub fn init(mgr:&mut ModulesManager)
|
||||||
alias : vec![String::from("tester1"),String::from("testy1")], // String of alternative names
|
alias : vec![String::from("tester1"),String::from("testy1")], // String of alternative names
|
||||||
exec_body : actions_util::asyncbox(testy) ,
|
exec_body : actions_util::asyncbox(testy) ,
|
||||||
help : String::from("DUPCMD4 tester"),
|
help : String::from("DUPCMD4 tester"),
|
||||||
required_roles : vec![],
|
required_roles : vec![
|
||||||
|
//identity::UserRole::Mod(ChType::Channel(String::new())),
|
||||||
|
identity::UserRole::SupMod(ChType::Channel(String::new()))
|
||||||
|
],
|
||||||
}.add_to_modmgr(mgr);
|
}.add_to_modmgr(mgr);
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,19 +45,21 @@ pub fn init(mgr:&mut ModulesManager)
|
||||||
alias : vec![String::from("tester2"),String::from("testy2")], // String of alternative names
|
alias : vec![String::from("tester2"),String::from("testy2")], // String of alternative names
|
||||||
exec_body : actions_util::asyncbox(testy) ,
|
exec_body : actions_util::asyncbox(testy) ,
|
||||||
help : String::from("DUPCMD4 tester"),
|
help : String::from("DUPCMD4 tester"),
|
||||||
required_roles : vec![],
|
required_roles : vec![
|
||||||
|
identity::UserRole::BotAdmin
|
||||||
|
],
|
||||||
}.add_to_modmgr(mgr);
|
}.add_to_modmgr(mgr);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let list1 = Listener {
|
// let list1 = Listener {
|
||||||
module : BotModule(String::from("experiments 004")),
|
// module : BotModule(String::from("experiments 004")),
|
||||||
name : String::from("GoodGirl Listener"),
|
// name : String::from("GoodGirl Listener"),
|
||||||
exec_body : actions_util::asyncbox(good_girl) ,
|
// exec_body : actions_util::asyncbox(good_girl) ,
|
||||||
help : String::from("")
|
// help : String::from("")
|
||||||
};
|
// };
|
||||||
|
|
||||||
list1.add_to_modmgr(mgr);
|
// list1.add_to_modmgr(mgr);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue