1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use anyhow::{anyhow, Result};
use irc::client::data::AccessLevel;
use irc::client::prelude::*;
use log::debug;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::fs;
use tokio::sync::RwLock;
use tokio::time::{interval, timeout, Duration};

pub mod channel;
pub mod chanserv;
pub mod command;
pub mod config;
pub mod git;

pub type LockedState = Arc<RwLock<BotState>>;

use channel::ConfiguredChannel;
use config::TrustLevel;

#[derive(Default)]
pub struct BotState {
    /// What we're currently PMing ChanServ for
    pub chanserv: Option<chanserv::Message>,
    /// State of channels we're currently looking up
    pub channels: HashMap<String, channel::ManagedChannel>,
    pub botconfig: config::BotConfig,
}

impl BotState {
    pub fn is_channel_done(&self, channel: &str) -> bool {
        if let Some(managed_channel) = self.channels.get(channel) {
            managed_channel.is_done()
        } else {
            false
        }
    }

    pub fn is_flags_done(&self, channel: &str) -> bool {
        if let Some(managed_channel) = self.channels.get(channel) {
            managed_channel.flags_done
        } else {
            false
        }
    }

    /// Whether the given username is a founder.
    /// NOTE: you need to check that flags_done is true first
    pub fn is_founder_on(&self, channel: &str, username: &str) -> bool {
        if let Some(managed_channel) = self.channels.get(channel) {
            if let Some(flags) = managed_channel.current.get(username) {
                return flags.contains(&'F');
            }
        }
        false
    }
}

/// Ask ChanServ for ops in a channel and wait till its set
async fn wait_for_op(client: &Client, channel: &str) -> Result<()> {
    let tmt =
        timeout(Duration::from_secs(5), _wait_for_op(client, channel)).await;
    if tmt.is_err() {
        debug!("Timeout getting ops for {}", channel);
        Err(anyhow!("Unable to get opped in {}", channel))
    } else {
        Ok(())
    }
}

async fn _wait_for_op(client: &Client, channel: &str) {
    if !is_opped_in(client, channel) {
        debug!("Getting ops in {}", channel);
        client
            .send_privmsg("ChanServ", format!("op {}", channel))
            // unwrap: OK, panic if we can't send messages
            .unwrap();
    } else {
        // Already opped!
        return;
    }
    // Wait until we are
    let mut interval = interval(Duration::from_millis(200));
    loop {
        if is_opped_in(client, channel) {
            break;
        }
        debug!("Not opped in {} yet.", channel);
        interval.tick().await;
    }
}

/// Read channel config from the directory
async fn read_channel_config(
    dir: &str,
    channel: &str,
) -> Result<ConfiguredChannel> {
    Ok(toml::from_str(
        &fs::read_to_string(format!(
            "{}/channels/{}.toml",
            dir,
            channel.trim_start_matches('#')
        ))
        .await?,
    )?)
}

fn is_opped_in(client: &Client, channel: &str) -> bool {
    if let Some(users) = client.list_users(channel) {
        for user in users {
            if user.get_nickname() == client.current_nickname() {
                return user.access_levels().contains(&AccessLevel::Oper);
            }
        }
    }

    // Not found in channel
    false
}

/// Given a message, extract the account of the sender
/// using the "account-tags" IRCv3 capability
pub fn extract_account(message: &Message) -> Option<String> {
    if let Some(tags) = &message.tags {
        for tag in tags {
            if tag.0 == "account" {
                if let Some(name) = &tag.1 {
                    return Some(name.to_string());
                }
            }
        }
    }

    None
}

/// Whether the given message was sent from someone who
/// is in our configured owners or trusted lists
pub async fn is_trusted(
    state: &LockedState,
    message: &Message,
    level: TrustLevel,
) -> bool {
    if let Some(account) = extract_account(message) {
        let list = match level {
            TrustLevel::Owner => state.read().await.botconfig.owners.clone(),
            TrustLevel::Trusted => state.read().await.botconfig.trusted.clone(),
        };
        list.contains(&account)
    } else {
        false
    }
}