<commit_before>//! A cargo subcommand for displaying line counts of source code in projects,
//! including a niave `unsafe` counter for Rust source files. This subcommand
//! was originally based off and inspired by the project
//! [tokei](https://github.com/aaronepower/tokei) by
//! [Aaronepower](https://github.com/aaronepower)
//!
//! ## Demo
//!
//! To count the source code in the [Rust](https://github.com/rust-lang/rust)
//! repository (checkout `4c99649`), and print some naive statistics on how much
//! "unsafe" code exists.
//!
//! **NOTE:** The Rust repository is quite large, if you're on a slow internet
//! connection consider using a smaller repository, such as the `cargo-count`
//! repo.
//!
//! ```ignore
//! $ git clone https://github.com/rust-lang/rust
//! $ cd rust
//! $ cargo count --separator , --unsafe-statistics
//! Gathering information...
//!          Language    Files  Lines    Blanks  Comments  Code     Unsafe (%)
//!          --------    -----  -----    ------  --------  ----     ----------
//! Rust        6,018  528,510  66,984  133,698   327,792  3,163
//! (0.96%)
//! C           54     9,962    1,445   1,492     7,025    7,025
//! (100.00%)
//!          CSS         4      1,266    149     52        1,065
//!          JavaScript  4      1,118    131     166       821
//!          Python      31     4,797    843     585       3,369
//! C Header    13     1,865    284     585       996      996
//! (100.00%)
//! C++         4      1,611    185     81        1,345    1,345
//! (100.00%)
//!          --------    -----  -----    ------  --------  ----     ----------
//! Totals:              6,128  549,129  70,021  136,659   342,413  12,529
//! (3.66%)
//!
//! ```
//!
//! The `--separator ,` sets a `,` character as the thousands separator, and
//! `--unsafe-statistics` looks for, and counts lines of `unsafe`.
//!
//! ## Compiling
//!
//! Follow these instructions to compile `cargo-count`, then skip down to
//! Installation.
//!
//!  1. Ensure you have current version of `cargo` and
//!     [Rust](https://www.rust-lang.org) installed
//!  2. Clone the project
//!     `$ git clone https://github.com/kbknapp/cargo-count && cd cargo-count`
//!  3. Build the project `$ cargo build --release` (**NOTE:** There is a large
//!     performance differnce when compiling without optimizations, so I
//!     recommend alwasy using `--release` to enable to them)
//!  4. Once complete, the binary will be located at
//!     `target/release/cargo-count`
//!
//! ## Installation and Usage
//!
//! All you need to do is place `cargo-count` somewhere in your `$PATH`. Then
//! run `cargo count` anywhere in your project directory. For full details see
//! below.
//!
//! ### Linux / OS X
//!
//! You have two options, place `cargo-count` into a directory that is already
//! located in your `$PATH` variable (To see which directories those are, open
//! a terminal and type `echo "${PATH//:/\n}"`, the quotation marks are
//! important), or you can add a custom directory to your `$PATH`
//!
//! **Option 1**
//! If you have write permission to a directory listed in your `$PATH` or you
//! have root permission (or via `sudo`), simply copy the `cargo-count` to that
//! directory `# sudo cp cargo-count /usr/local/bin`
//!
//! **Option 2**
//! If you do not have root, `sudo`, or write permission to any directory
//! already in `$PATH` you can create a directory inside your home directory,
//! and add that. Many people use `$HOME/.bin` to keep it hidden (and not
//! clutter your home directory), or `$HOME/bin` if you want it to be always
//! visible. Here is an example to make the directory, add it to `$PATH`, and
//! copy `cargo-count` there.
//!
//! Simply change `bin` to whatever you'd like to name the directory, and
//! `.bashrc` to whatever your shell startup file is (usually `.bashrc`,
//! `.bash_profile`, or `.zshrc`)
//!
//! ```sh
//! $ mkdir ~/bin
//! $ echo "export PATH=$PATH:$HOME/bin" >> ~/.bashrc
//! $ cp cargo-count ~/bin
//! $ source ~/.bashrc
//! ```
//!
//! ### Windows
//!
//! On Windows 7/8 you can add directory to the `PATH` variable by opening a
//! command line as an administrator and running
//!
//! ```sh
//! C:\> setx path "%path%;C:\path\to\cargo-count\binary"
//! ```
//!
//! Otherwise, ensure you have the `cargo-count` binary in the directory which
//! you operating in the command line from, because Windows automatically adds
//! your current directory to PATH (i.e. if you open a command line to
//! `C:\my_project\` to use `cargo-count` ensure `cargo-count.exe` is inside
//! that directory as well).
//!
//!
//! ### Options
//!
//! There are a few options for using `cargo-count` which should be somewhat
//! self explanitory.
//!
//! ```ignore
//! USAGE:
//!     cargo count [FLAGS] [OPTIONS] [--] [ARGS]
//!
//! FLAGS:
//! -S, --follow-symlinks      Follows symlinks and counts source files it
//! finds
//!                                (Defaults to false when omitted)
//!     -h, --help                 Prints help information
//! --unsafe-statistics    Displays lines and percentages of "unsafe"
//! code
//!     -V, --version              Prints version information
//!     -v, --verbose              Print verbose output
//!
//! OPTIONS:
//! -l, --language <exts>...    Only count these languges (by source code
//! extension)
//!                                 (i.e. '-l js py cpp')
//! -e, --exclude <paths>...    Files or directories to exclude
//! (automatically includes '.git')
//! --utf8-rule <rule>      Sets the UTF-8 parsing rule (Defaults to
//! 'strict')
//!                                  [values: ignore lossy strict]
//! -s, --separator <sep>       Set the thousands separator for pretty
//! printing
//!
//! ARGS:
//!     to_count...    The files or directories (including children) to count
//!                    (defaults to current working directory when omitted)
//!
//! When using '--exclude <path>' the path given can either be relative to the
//! current
//! directory, or absolute. When '<path>' is a file, it must be relative to the
//! current
//! directory or it will not be found. Example, if the current directory has a
//! child
//! directory named 'target' with a child fild 'test.rs' and you use `--exclude
//! target/test.rs'
//!
//! Globs are also supported. For example, to eclude 'test.rs' files from all
//! child directories
//! of the current directory you could do '--exclude */test.rs'.
//! ```
//!
//! ## License
//!
//! `cargo-count` is released under the terms of the MIT. See the LICENSE-MIT
//! file for the details.
#![cfg_attr(feature = "unstable", feature(plugin))]
#![cfg_attr(feature = "lints", plugin(clippy))]
#![cfg_attr(feature = "lints", allow(explicit_iter_loop))]
#![cfg_attr(feature = "lints", allow(should_implement_trait))]
#![cfg_attr(feature = "lints", allow(unstable_features))]
#![cfg_attr(feature = "lints", deny(warnings))]
#![cfg_attr(not(any(feature = "lints", feature = "unstable")), deny(unstable_features))]
#![deny(missing_docs,
        missing_debug_implementations,
        missing_copy_implementations,
        trivial_casts, trivial_numeric_casts,
        unsafe_code,
        unused_import_braces,
        unused_qualifications)]

#[macro_use]
extern crate clap;
#[cfg(feature = "color")]
extern crate ansi_term;
extern crate tabwriter;
extern crate glob;
extern crate regex;

use std::io::Write;
#[cfg(feature = "debug")]
use std::env;

use clap::{App, AppSettings, Arg, SubCommand};

use config::Config;
use count::Counts;
use error::{CliError, CliResult};
use fmt::Format;

#[macro_use]
mod macros;
mod comment;
mod config;
mod count;
mod error;
mod fmt;
mod fsutil;
mod language;

static UTF8_RULES: [&'static str; 3] = ["strict", "lossy", "ignore"];

fn main() {
    debugln!("executing; cmd=cargo-count; args={:?}",
             env::args().collect::<Vec<_>>());
    let m = App::new("cargo-count")
        .version(&*format!("v{}", crate_version!()))
    // We have to lie about our binary name since this will be a third party
    // subcommand for cargo but we want usage strings to generated properly
        .bin_name("cargo")
    // Global version uses the version we supplied (Cargo.toml) for all subcommands
    // as well
        .settings(&[AppSettings::GlobalVersion,
                    AppSettings::SubcommandRequired])
    // We use a subcommand because everything parsed after `cargo` is sent to the
    // third party
    // plugin which will then be interpreted as a subcommand/positional arg by clap
        .subcommand(SubCommand::with_name("count")
            .author("Kevin K. <kbknapp@gmail.com>")
            .about("Displays line counts of code for cargo projects")
            .args_from_usage("-e, --exclude [paths]...    'Files or directories to exclude (automatically includes \'.git\')'
                              --unsafe-statistics         'Displays lines and percentages of \"unsafe\" code'
                              -l, --language [exts]...    'Only count these languges (by source code extension){n}\
                                                           (i.e. \'-l js py cpp\')'
                              -v, --verbose               'Print verbose output'
                              -S, --follow-symlinks       'Follows symlinks and counts source files it finds{n}(Defaults to false when omitted)'
                              [to_count]...               'The files or directories (including children) to count{n}\
                                                           (defaults to current working directory when omitted)'")
            .arg(Arg::from_usage("-s, --separator [sep]   'Set the thousands separator for pretty printing'")
                .validator(single_char))
            .arg(Arg::from_usage("--utf8-rule [rule]     'Sets the UTF-8 parsing rule (Defaults to \'strict\'){n}'")
                .possible_values(&UTF8_RULES))
            .after_help("When using '--exclude <path>' the path given can either be relative to the current \n\
                         directory, or absolute. When '<path>' is a file, it must be relative to the current \n\
                         directory or it will not be found. Example, if the current directory has a child \n\
                         directory named 'target' with a child fild 'test.rs' and you use `--exclude target/test.rs' \n\
                         \n\
                         Globs are also supported. For example, to eclude 'test.rs' files from all child directories \n\
                         of the current directory you could do '--exclude */test.rs'."))
        .get_matches();

    if let Some(m) = m.subcommand_matches("count") {
        let cfg = Config::from_matches(m).unwrap_or_else(|e| e.exit());
        println!("Gathering information...");
        if let Err(e) = execute(cfg) {
            e.exit();
        }
    }
}

fn execute(cfg: Config) -> CliResult<()> {
    debugln!("executing; cmd=execute;");
    verboseln!(cfg, "{}: {:?}", Format::Warning("Excluding"), cfg.exclude);
    verbose!(cfg,
             "{}",
             if cfg.exts.is_some() {
                 format!("{} including files with extension: {}\n",
                         Format::Warning("Only"),
                         cfg.exts
                            .as_ref()
                            .unwrap()
                            .join(", "))
             } else {
                 "".to_owned()
             });

    debugln!("Checking for files or dirs to count from cli");

    let mut counts = Counts::new(&cfg);
    counts.fill_from();
    cli_try!(counts.count());
    cli_try!(counts.write_results());
    Ok(())
}

fn single_char(s: String) -> Result<(), String> {
    if s.len() != 1 {
        Err(
          format!(
            "the --separator argument option only accepts a single character but found '{}'",
             Format::Warning(s)))
    } else {
        Ok(())
    }
}
<commit_msg>fix: fixes features declarations<commit_after>//! A cargo subcommand for displaying line counts of source code in projects,
//! including a niave `unsafe` counter for Rust source files. This subcommand
//! was originally based off and inspired by the project
//! [tokei](https://github.com/aaronepower/tokei) by
//! [Aaronepower](https://github.com/aaronepower)
//!
//! ## Demo
//!
//! To count the source code in the [Rust](https://github.com/rust-lang/rust)
//! repository (checkout `4c99649`), and print some naive statistics on how much
//! "unsafe" code exists.
//!
//! **NOTE:** The Rust repository is quite large, if you're on a slow internet
//! connection consider using a smaller repository, such as the `cargo-count`
//! repo.
//!
//! ```ignore
//! $ git clone https://github.com/rust-lang/rust
//! $ cd rust
//! $ cargo count --separator , --unsafe-statistics
//! Gathering information...
//!          Language    Files  Lines    Blanks  Comments  Code     Unsafe (%)
//!          --------    -----  -----    ------  --------  ----     ----------
//! Rust        6,018  528,510  66,984  133,698   327,792  3,163
//! (0.96%)
//! C           54     9,962    1,445   1,492     7,025    7,025
//! (100.00%)
//!          CSS         4      1,266    149     52        1,065
//!          JavaScript  4      1,118    131     166       821
//!          Python      31     4,797    843     585       3,369
//! C Header    13     1,865    284     585       996      996
//! (100.00%)
//! C++         4      1,611    185     81        1,345    1,345
//! (100.00%)
//!          --------    -----  -----    ------  --------  ----     ----------
//! Totals:              6,128  549,129  70,021  136,659   342,413  12,529
//! (3.66%)
//!
//! ```
//!
//! The `--separator ,` sets a `,` character as the thousands separator, and
//! `--unsafe-statistics` looks for, and counts lines of `unsafe`.
//!
//! ## Compiling
//!
//! Follow these instructions to compile `cargo-count`, then skip down to
//! Installation.
//!
//!  1. Ensure you have current version of `cargo` and
//!     [Rust](https://www.rust-lang.org) installed
//!  2. Clone the project
//!     `$ git clone https://github.com/kbknapp/cargo-count && cd cargo-count`
//!  3. Build the project `$ cargo build --release` (**NOTE:** There is a large
//!     performance differnce when compiling without optimizations, so I
//!     recommend alwasy using `--release` to enable to them)
//!  4. Once complete, the binary will be located at
//!     `target/release/cargo-count`
//!
//! ## Installation and Usage
//!
//! All you need to do is place `cargo-count` somewhere in your `$PATH`. Then
//! run `cargo count` anywhere in your project directory. For full details see
//! below.
//!
//! ### Linux / OS X
//!
//! You have two options, place `cargo-count` into a directory that is already
//! located in your `$PATH` variable (To see which directories those are, open
//! a terminal and type `echo "${PATH//:/\n}"`, the quotation marks are
//! important), or you can add a custom directory to your `$PATH`
//!
//! **Option 1**
//! If you have write permission to a directory listed in your `$PATH` or you
//! have root permission (or via `sudo`), simply copy the `cargo-count` to that
//! directory `# sudo cp cargo-count /usr/local/bin`
//!
//! **Option 2**
//! If you do not have root, `sudo`, or write permission to any directory
//! already in `$PATH` you can create a directory inside your home directory,
//! and add that. Many people use `$HOME/.bin` to keep it hidden (and not
//! clutter your home directory), or `$HOME/bin` if you want it to be always
//! visible. Here is an example to make the directory, add it to `$PATH`, and
//! copy `cargo-count` there.
//!
//! Simply change `bin` to whatever you'd like to name the directory, and
//! `.bashrc` to whatever your shell startup file is (usually `.bashrc`,
//! `.bash_profile`, or `.zshrc`)
//!
//! ```sh
//! $ mkdir ~/bin
//! $ echo "export PATH=$PATH:$HOME/bin" >> ~/.bashrc
//! $ cp cargo-count ~/bin
//! $ source ~/.bashrc
//! ```
//!
//! ### Windows
//!
//! On Windows 7/8 you can add directory to the `PATH` variable by opening a
//! command line as an administrator and running
//!
//! ```sh
//! C:\> setx path "%path%;C:\path\to\cargo-count\binary"
//! ```
//!
//! Otherwise, ensure you have the `cargo-count` binary in the directory which
//! you operating in the command line from, because Windows automatically adds
//! your current directory to PATH (i.e. if you open a command line to
//! `C:\my_project\` to use `cargo-count` ensure `cargo-count.exe` is inside
//! that directory as well).
//!
//!
//! ### Options
//!
//! There are a few options for using `cargo-count` which should be somewhat
//! self explanitory.
//!
//! ```ignore
//! USAGE:
//!     cargo count [FLAGS] [OPTIONS] [--] [ARGS]
//!
//! FLAGS:
//! -S, --follow-symlinks      Follows symlinks and counts source files it
//! finds
//!                                (Defaults to false when omitted)
//!     -h, --help                 Prints help information
//! --unsafe-statistics    Displays lines and percentages of "unsafe"
//! code
//!     -V, --version              Prints version information
//!     -v, --verbose              Print verbose output
//!
//! OPTIONS:
//! -l, --language <exts>...    Only count these languges (by source code
//! extension)
//!                                 (i.e. '-l js py cpp')
//! -e, --exclude <paths>...    Files or directories to exclude
//! (automatically includes '.git')
//! --utf8-rule <rule>      Sets the UTF-8 parsing rule (Defaults to
//! 'strict')
//!                                  [values: ignore lossy strict]
//! -s, --separator <sep>       Set the thousands separator for pretty
//! printing
//!
//! ARGS:
//!     to_count...    The files or directories (including children) to count
//!                    (defaults to current working directory when omitted)
//!
//! When using '--exclude <path>' the path given can either be relative to the
//! current
//! directory, or absolute. When '<path>' is a file, it must be relative to the
//! current
//! directory or it will not be found. Example, if the current directory has a
//! child
//! directory named 'target' with a child fild 'test.rs' and you use `--exclude
//! target/test.rs'
//!
//! Globs are also supported. For example, to eclude 'test.rs' files from all
//! child directories
//! of the current directory you could do '--exclude */test.rs'.
//! ```
//!
//! ## License
//!
//! `cargo-count` is released under the terms of the MIT. See the LICENSE-MIT
//! file for the details.
#![cfg_attr(feature = "nightly", feature(plugin))]
#![cfg_attr(feature = "lints", plugin(clippy))]
#![cfg_attr(feature = "lints", allow(explicit_iter_loop))]
#![cfg_attr(feature = "lints", allow(should_implement_trait))]
#![cfg_attr(feature = "lints", allow(unstable_features))]
#![cfg_attr(feature = "lints", deny(warnings))]
#![cfg_attr(not(any(feature = "nightly", feature = "unstable")), deny(unstable_features))]
#![deny(missing_docs,
        missing_debug_implementations,
        missing_copy_implementations,
        trivial_casts, trivial_numeric_casts,
        unsafe_code,
        unused_import_braces,
        unused_qualifications)]

#[macro_use]
extern crate clap;
#[cfg(feature = "color")]
extern crate ansi_term;
extern crate tabwriter;
extern crate glob;
extern crate regex;

use std::io::Write;
#[cfg(feature = "debug")]
use std::env;

use clap::{App, AppSettings, Arg, SubCommand};

use config::Config;
use count::Counts;
use error::{CliError, CliResult};
use fmt::Format;

#[macro_use]
mod macros;
mod comment;
mod config;
mod count;
mod error;
mod fmt;
mod fsutil;
mod language;

static UTF8_RULES: [&'static str; 3] = ["strict", "lossy", "ignore"];

fn main() {
    debugln!("executing; cmd=cargo-count; args={:?}",
             env::args().collect::<Vec<_>>());
    let m = App::new("cargo-count")
        .version(&*format!("v{}", crate_version!()))
    // We have to lie about our binary name since this will be a third party
    // subcommand for cargo but we want usage strings to generated properly
        .bin_name("cargo")
    // Global version uses the version we supplied (Cargo.toml) for all subcommands
    // as well
        .settings(&[AppSettings::GlobalVersion,
                    AppSettings::SubcommandRequired])
    // We use a subcommand because everything parsed after `cargo` is sent to the
    // third party
    // plugin which will then be interpreted as a subcommand/positional arg by clap
        .subcommand(SubCommand::with_name("count")
            .author("Kevin K. <kbknapp@gmail.com>")
            .about("Displays line counts of code for cargo projects")
            .args_from_usage("-e, --exclude [paths]...    'Files or directories to exclude (automatically includes \'.git\')'
                              --unsafe-statistics         'Displays lines and percentages of \"unsafe\" code'
                              -l, --language [exts]...    'Only count these languges (by source code extension){n}\
                                                           (i.e. \'-l js py cpp\')'
                              -v, --verbose               'Print verbose output'
                              -S, --follow-symlinks       'Follows symlinks and counts source files it finds{n}(Defaults to false when omitted)'
                              [to_count]...               'The files or directories (including children) to count{n}\
                                                           (defaults to current working directory when omitted)'")
            .arg(Arg::from_usage("-s, --separator [sep]   'Set the thousands separator for pretty printing'")
                .validator(single_char))
            .arg(Arg::from_usage("--utf8-rule [rule]     'Sets the UTF-8 parsing rule (Defaults to \'strict\'){n}'")
                .possible_values(&UTF8_RULES))
            .after_help("When using '--exclude <path>' the path given can either be relative to the current \n\
                         directory, or absolute. When '<path>' is a file, it must be relative to the current \n\
                         directory or it will not be found. Example, if the current directory has a child \n\
                         directory named 'target' with a child fild 'test.rs' and you use `--exclude target/test.rs' \n\
                         \n\
                         Globs are also supported. For example, to eclude 'test.rs' files from all child directories \n\
                         of the current directory you could do '--exclude */test.rs'."))
        .get_matches();

    if let Some(m) = m.subcommand_matches("count") {
        let cfg = Config::from_matches(m).unwrap_or_else(|e| e.exit());
        println!("Gathering information...");
        if let Err(e) = execute(cfg) {
            e.exit();
        }
    }
}

fn execute(cfg: Config) -> CliResult<()> {
    debugln!("executing; cmd=execute;");
    verboseln!(cfg, "{}: {:?}", Format::Warning("Excluding"), cfg.exclude);
    verbose!(cfg,
             "{}",
             if cfg.exts.is_some() {
                 format!("{} including files with extension: {}\n",
                         Format::Warning("Only"),
                         cfg.exts
                            .as_ref()
                            .unwrap()
                            .join(", "))
             } else {
                 "".to_owned()
             });

    debugln!("Checking for files or dirs to count from cli");

    let mut counts = Counts::new(&cfg);
    counts.fill_from();
    cli_try!(counts.count());
    cli_try!(counts.write_results());
    Ok(())
}

fn single_char(s: String) -> Result<(), String> {
    if s.len() != 1 {
        Err(
          format!(
            "the --separator argument option only accepts a single character but found '{}'",
             Format::Warning(s)))
    } else {
        Ok(())
    }
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate log;
extern crate env_logger;
extern crate irc;
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate hyper;
extern crate hubcaps;
extern crate hyper_native_tls;
extern crate serde_json;

use std::env;
use std::fmt;
use std::thread;
use std::collections::HashMap;
use regex::Regex;

use irc::client::prelude::*;
use irc::client::data::command::Command;

use hyper::Client;
use hyper::net::HttpsConnector;
use hyper_native_tls::NativeTlsClient;
use hubcaps::{Credentials, Github};
use hubcaps::comments::CommentOptions;

fn main() {
    env_logger::init().unwrap();

    let config_file =
        {
            let mut args = env::args_os().skip(1); // skip program name
            let config_file = args.next().expect("Expected a single command-line argument, the JSON configuration file.");
            if args.next().is_some() {
                panic!("Expected only a single command-line argument, the JSON configuration file.");
            }
            config_file
        };

    let server = IrcServer::new(config_file).expect("Couldn't initialize server with given configuration file");
    server.identify().unwrap();

    let options = server
        .config()
        .options
        .as_ref()
        .expect("No options property within configuration?");

    // FIXME: Add a way to ask the bot to reboot itself?
    let mut channel_data: HashMap<String, ChannelData> = HashMap::new();

    for message in server.iter() {
        let message = message.unwrap(); // panic if there's an error

        match message.command {
            Command::PRIVMSG(ref target, ref msg) => {
                match message.source_nickname() {
                    None => {
                        warn!("PRIVMSG without a source! {}",
                              format!("{}", message).trim());
                    }
                    Some(ref source) => {
                        let source_ = String::from(*source);
                        let line = if msg.starts_with("\x01ACTION ") && msg.ends_with("\x01") {
                            ChannelLine {
                                source: source_,
                                is_action: true,
                                message: String::from(&msg[8..msg.len() - 1]),
                            }
                        } else {
                            ChannelLine {
                                source: source_,
                                is_action: false,
                                message: msg.clone(),
                            }
                        };
                        let mynick = server.current_nickname();
                        if target == mynick {
                            // An actual private message.
                            info!("[{}] {}", source, line);
                            handle_bot_command(&server,
                                               options,
                                               &mut channel_data,
                                               &line.message,
                                               source,
                                               false,
                                               None)
                        } else if target.starts_with('#') {
                            // A message in a channel.
                            info!("[{}] {}", target, line);
                            match check_command_in_channel(mynick, &line.message) {
                                Some(ref command) => {
                                    handle_bot_command(&server,
                                                       options,
                                                       &mut channel_data,
                                                       command,
                                                       target,
                                                       line.is_action,
                                                       Some(source))
                                }
                                None => {
                                    // FIXME: code duplication
                                    let this_channel_data =
                                        channel_data
                                            .entry(target.clone())
                                            .or_insert_with(|| ChannelData::new(options));
                                    if let Some(response) = this_channel_data.add_line(line) {
                                        server.send_privmsg(target, &*response).unwrap();
                                    }
                                }
                            }
                        } else {
                            warn!("UNEXPECTED TARGET {} in message {}",
                                  target,
                                  format!("{}", message).trim());
                        }
                    }
                }
            }
            Command::INVITE(ref target, ref channel) => {
                if target == server.current_nickname() {
                    // Join channels when invited.
                    server.send_join(channel).unwrap();
                }
            }
            _ => (),
        }
    }
}

// Take a message in the channel, and see if it was a message sent to
// this bot.
fn check_command_in_channel(mynick: &str, msg: &String) -> Option<String> {
    if !msg.starts_with(mynick) {
        return None;
    }
    let after_nick = &msg[mynick.len()..];
    if !after_nick.starts_with(":") && !after_nick.starts_with(",") {
        return None;
    }
    let after_punct = &after_nick[1..];
    Some(String::from(after_punct.trim_left()))
}

fn handle_bot_command<'opts>(server: &IrcServer,
                             options: &'opts HashMap<String, String>,
                             all_channel_data: &mut HashMap<String, ChannelData<'opts>>,
                             command: &str,
                             response_target: &str,
                             response_is_action: bool,
                             response_username: Option<&str>) {

    let send_line = |response_username: Option<&str>, line: &str| {
        let line_with_nick = match response_username {
            None => String::from(line),
            Some(username) => String::from(username) + ", " + line,
        };
        let adjusted_line = if response_is_action {
            format!("\x01ACTION {}\x01", line_with_nick)
        } else {
            line_with_nick
        };
        server
            .send_privmsg(response_target, &adjusted_line)
            .unwrap();
    };

    if command == "help" {
        send_line(response_username, "The commands I understand are:");
        send_line(None, "  help     Send this message.");
        send_line(None, "  intro    Send a message describing what I do.");
        send_line(None, "  status   Send a message with current bot status.");
        send_line(None,
                  "  bye      Leave the channel.  (You can /invite me back.)");
        return;
    }

    if command == "intro" {
        let config = server.config();
        send_line(None,
                  "My job is to leave comments in github when the group discusses github issues and takes minutes in IRC.");
        send_line(None,
                  "I separate discussions by the \"Topic:\" lines, and I know what github issues to use only by lines of the form \"Topic github: <url>\", or if the \"Topic:\" line is a github URL.");
        send_line(None,
                  &*format!("I'm only allowed to comment on issues in the repositories: {}.",
                            options["github_repos_allowed"]));
        let owners = if let Some(v) = config.owners.as_ref() {
            v.join(" ")
        } else {
            String::from("")
        };
        send_line(None,
                  &*format!("My source code is at {} and I'm run by {}.",
                            options["source"],
                            owners));
        return;
    }

    if command == "status" {
        send_line(response_username,
                  "I currently have data for the following channels:");
        let mut sorted_channels: Vec<&String> = all_channel_data.keys().collect();
        sorted_channels.sort();
        for channel in sorted_channels {
            let ref channel_data = all_channel_data[channel];
            if let Some(ref topic) = channel_data.current_topic {
                send_line(None,
                          &*format!("  {} ({} lines buffered on \"{}\")",
                                    channel,
                                    topic.lines.len(),
                                    topic.topic));
                match topic.github_url {
                    None => send_line(None, "    no GitHub URL to comment on"),
                    Some(ref github_url) => {
                        send_line(None, &*format!("    will comment on {}", github_url))
                    }
                };
            } else {
                send_line(None, &*format!("  {} (no topic data buffered)", channel));
            }
        }
        return;
    }

    if command == "bye" {
        if response_target.starts_with('#') {
            // FIXME: code duplication
            let this_channel_data = all_channel_data
                .entry(String::from(response_target))
                .or_insert_with(|| ChannelData::new(options));
            this_channel_data.end_topic();
            server.send(Command::PART(String::from(response_target),
                        Some(format!("Leaving at request of {}.  Feel free to /invite me back.",
                                     response_username.unwrap())))).unwrap();
        } else {
            send_line(response_username, "'bye' only works in a channel");
        }
        return;
    }

    send_line(response_username,
              "Sorry, I don't understand that command.  Try 'help'.");
}

struct ChannelLine {
    source: String,
    is_action: bool,
    message: String,
}

struct TopicData {
    topic: String,
    github_url: Option<String>,
    lines: Vec<ChannelLine>,
    resolutions: Vec<String>,
}

struct ChannelData<'opts> {
    current_topic: Option<TopicData>,
    options: &'opts HashMap<String, String>,
}

impl fmt::Display for ChannelLine {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.is_action {
            write!(f, "* {} {}", self.source, self.message)
        } else {
            write!(f, "<{}> {}", self.source, self.message)
        }
    }
}

impl TopicData {
    fn new(topic: &str) -> TopicData {
        let topic_ = String::from(topic);
        TopicData {
            topic: topic_,
            github_url: None,
            lines: vec![],
            resolutions: vec![],
        }
    }
}

impl fmt::Display for TopicData {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.resolutions.len() == 0 {
            try!(write!(f,
                        "The CSS Working Group just discussed {}.\n\n",
                        self.topic));
        } else {
            try!(write!(f,
                        "The CSS Working Group just discussed {}, and agreed to the following resolutions:\n\n",
                        self.topic));
            for resolution in &self.resolutions {
                try!(write!(f, "```\n{}\n```\n\n", resolution));
            }
        }

        try!(write!(f,
                    "The full IRC log of that discussion\n"));
        try!(write!(f, "\n```\n"));
        for line in &self.lines {
            try!(write!(f, "{}\n", line));
        }
        try!(write!(f, "```\n\n"));
        Ok(())
    }
}

/// A case-insensitive version of starts_with.
fn ci_starts_with(s: &str, prefix: &str) -> bool {
    assert!(prefix.to_lowercase() == prefix);

    s.len() >= prefix.len() && s[0..prefix.len()].to_lowercase() == prefix
}

/// Remove a case-insensitive start of the line, and if that prefix is
/// present return the rest of the line.
fn strip_ci_prefix(s: &str, prefix: &str) -> Option<String> {
    if ci_starts_with(s, prefix) {
        Some(String::from(s[prefix.len()..].trim_left()))
    } else {
        None
    }
}

impl<'opts> ChannelData<'opts> {
    fn new(options_: &'opts HashMap<String, String>) -> ChannelData {
        ChannelData {
            current_topic: None,
            options: options_,
        }
    }

    // Returns the response that should be sent to the message over IRC.
    fn add_line(&mut self, line: ChannelLine) -> Option<String> {
        if let Some(ref topic) = strip_ci_prefix(&line.message, "topic:") {
            self.start_topic(topic);
        }
        if line.source == "trackbot" && line.is_action == true &&
           line.message == "is ending a teleconference." {
            self.end_topic();
        }
        match self.current_topic {
            None => None,
            Some(ref mut data) => {
                let new_url_option = extract_github_url(&line.message, self.options);
                let response = match (new_url_option.as_ref(), data.github_url.as_ref()) {
                    (None, _) => None,
                    // FIXME: Add and implement instructions to cancel!
                    (Some(new_url), None) => {
                        Some(format!("OK, I'll post this discussion to {}", new_url))
                    }
                    (Some(new_url), Some(old_url)) if old_url == new_url => None,
                    (Some(new_url), Some(old_url)) => {
                        Some(format!("OK, I'll post this discussion to {} instead of {} like you said before",
                                     new_url,
                                     old_url))
                    }
                };

                if let Some(new_url) = new_url_option {
                    data.github_url = Some(new_url);
                }

                if line.message.starts_with("RESOLUTION") || line.message.starts_with("RESOLVED") {
                    data.resolutions.push(line.message.clone());
                }

                data.lines.push(line);

                response
            }
        }
    }

    fn start_topic(&mut self, topic: &str) {
        self.end_topic();
        self.current_topic = Some(TopicData::new(topic));
    }

    fn end_topic(&mut self) {
        // TODO: Test the topic boundary code.
        if let Some(topic) = self.current_topic.take() {
            if topic.github_url.is_some() {
                let task = GithubCommentTask::new(topic, self.options);
                task.run();
            }
        }
    }
}

fn extract_github_url(message: &str, options: &HashMap<String, String>) -> Option<String> {
    lazy_static! {
        static ref GITHUB_URL_RE: Regex =
            Regex::new(r"^https://github.com/(?P<repo>[^/]*/[^/]*)/issues/(?P<number>[0-9]+)$")
            .unwrap();
    }
    let ref allowed_repos = options["github_repos_allowed"];
    for prefix in ["topic:", "github topic:"].into_iter() {
        if let Some(ref maybe_url) = strip_ci_prefix(&message, prefix) {
            if let Some(ref caps) = GITHUB_URL_RE.captures(maybe_url) {
                for repo in allowed_repos.split_whitespace() {
                    if caps["repo"] == *repo {
                        return Some(maybe_url.clone());
                    }
                }
            }
        }
    }
    None
}

struct GithubCommentTask {
    data: TopicData,
    github: Github,
}

impl GithubCommentTask {
    fn new(data_: TopicData, options: &HashMap<String, String>) -> GithubCommentTask {
        let github_ =
            Github::new(&*options["github_uastring"],
                        Client::with_connector(HttpsConnector::new(NativeTlsClient::new()
                                                                       .unwrap())),
                        Credentials::Token(options["github_access_token"].clone()));
        GithubCommentTask {
            data: data_,
            github: github_,
        }
    }
    fn run(self) {
        thread::spawn(move || { self.main(); });
    }
    fn main(&self) {
        lazy_static! {
            static ref GITHUB_URL_RE: Regex =
                Regex::new(r"^https://github.com/(?P<owner>[^/]*)/(?P<repo>[^/]*)/issues/(?P<number>[0-9]+)$")
                .unwrap();
        }

        if let Some(ref github_url) = self.data.github_url {
            if let Some(ref caps) = GITHUB_URL_RE.captures(github_url) {
                let repo = self.github
                    .repo(String::from(&caps["owner"]), String::from(&caps["repo"]));
                let issue = repo.issue(caps["number"].parse::<u64>().unwrap());
                let comments = issue.comments();

                let comment_text = format!("{}", self.data);
                comments
                    .create(&CommentOptions { body: comment_text })
                    .unwrap();
            } else {
                warn!("How does {} fail to match now when it matched before?",
                      github_url)
            }
        }
    }
}
<commit_msg>Implement "GitHub topic: none" to undo setting topic.<commit_after>#[macro_use]
extern crate log;
extern crate env_logger;
extern crate irc;
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate hyper;
extern crate hubcaps;
extern crate hyper_native_tls;
extern crate serde_json;

use std::env;
use std::fmt;
use std::thread;
use std::collections::HashMap;
use regex::Regex;

use irc::client::prelude::*;
use irc::client::data::command::Command;

use hyper::Client;
use hyper::net::HttpsConnector;
use hyper_native_tls::NativeTlsClient;
use hubcaps::{Credentials, Github};
use hubcaps::comments::CommentOptions;

fn main() {
    env_logger::init().unwrap();

    let config_file =
        {
            let mut args = env::args_os().skip(1); // skip program name
            let config_file = args.next().expect("Expected a single command-line argument, the JSON configuration file.");
            if args.next().is_some() {
                panic!("Expected only a single command-line argument, the JSON configuration file.");
            }
            config_file
        };

    let server = IrcServer::new(config_file).expect("Couldn't initialize server with given configuration file");
    server.identify().unwrap();

    let options = server
        .config()
        .options
        .as_ref()
        .expect("No options property within configuration?");

    // FIXME: Add a way to ask the bot to reboot itself?
    let mut channel_data: HashMap<String, ChannelData> = HashMap::new();

    for message in server.iter() {
        let message = message.unwrap(); // panic if there's an error

        match message.command {
            Command::PRIVMSG(ref target, ref msg) => {
                match message.source_nickname() {
                    None => {
                        warn!("PRIVMSG without a source! {}",
                              format!("{}", message).trim());
                    }
                    Some(ref source) => {
                        let source_ = String::from(*source);
                        let line = if msg.starts_with("\x01ACTION ") && msg.ends_with("\x01") {
                            ChannelLine {
                                source: source_,
                                is_action: true,
                                message: String::from(&msg[8..msg.len() - 1]),
                            }
                        } else {
                            ChannelLine {
                                source: source_,
                                is_action: false,
                                message: msg.clone(),
                            }
                        };
                        let mynick = server.current_nickname();
                        if target == mynick {
                            // An actual private message.
                            info!("[{}] {}", source, line);
                            handle_bot_command(&server,
                                               options,
                                               &mut channel_data,
                                               &line.message,
                                               source,
                                               false,
                                               None)
                        } else if target.starts_with('#') {
                            // A message in a channel.
                            info!("[{}] {}", target, line);
                            match check_command_in_channel(mynick, &line.message) {
                                Some(ref command) => {
                                    handle_bot_command(&server,
                                                       options,
                                                       &mut channel_data,
                                                       command,
                                                       target,
                                                       line.is_action,
                                                       Some(source))
                                }
                                None => {
                                    // FIXME: code duplication
                                    let this_channel_data =
                                        channel_data
                                            .entry(target.clone())
                                            .or_insert_with(|| ChannelData::new(options));
                                    if let Some(response) = this_channel_data.add_line(line) {
                                        server.send_privmsg(target, &*response).unwrap();
                                    }
                                }
                            }
                        } else {
                            warn!("UNEXPECTED TARGET {} in message {}",
                                  target,
                                  format!("{}", message).trim());
                        }
                    }
                }
            }
            Command::INVITE(ref target, ref channel) => {
                if target == server.current_nickname() {
                    // Join channels when invited.
                    server.send_join(channel).unwrap();
                }
            }
            _ => (),
        }
    }
}

// Take a message in the channel, and see if it was a message sent to
// this bot.
fn check_command_in_channel(mynick: &str, msg: &String) -> Option<String> {
    if !msg.starts_with(mynick) {
        return None;
    }
    let after_nick = &msg[mynick.len()..];
    if !after_nick.starts_with(":") && !after_nick.starts_with(",") {
        return None;
    }
    let after_punct = &after_nick[1..];
    Some(String::from(after_punct.trim_left()))
}

fn handle_bot_command<'opts>(server: &IrcServer,
                             options: &'opts HashMap<String, String>,
                             all_channel_data: &mut HashMap<String, ChannelData<'opts>>,
                             command: &str,
                             response_target: &str,
                             response_is_action: bool,
                             response_username: Option<&str>) {

    let send_line = |response_username: Option<&str>, line: &str| {
        let line_with_nick = match response_username {
            None => String::from(line),
            Some(username) => String::from(username) + ", " + line,
        };
        let adjusted_line = if response_is_action {
            format!("\x01ACTION {}\x01", line_with_nick)
        } else {
            line_with_nick
        };
        server
            .send_privmsg(response_target, &adjusted_line)
            .unwrap();
    };

    if command == "help" {
        send_line(response_username, "The commands I understand are:");
        send_line(None, "  help     Send this message.");
        send_line(None, "  intro    Send a message describing what I do.");
        send_line(None, "  status   Send a message with current bot status.");
        send_line(None,
                  "  bye      Leave the channel.  (You can /invite me back.)");
        return;
    }

    if command == "intro" {
        let config = server.config();
        send_line(None,
                  "My job is to leave comments in github when the group discusses github issues and takes minutes in IRC.");
        send_line(None,
                  "I separate discussions by the \"Topic:\" lines, and I know what github issues to use only by lines of the form \"GitHub topic: <url> | none\", or if the \"Topic:\" line is a github URL.");
        send_line(None,
                  &*format!("I'm only allowed to comment on issues in the repositories: {}.",
                            options["github_repos_allowed"]));
        let owners = if let Some(v) = config.owners.as_ref() {
            v.join(" ")
        } else {
            String::from("")
        };
        send_line(None,
                  &*format!("My source code is at {} and I'm run by {}.",
                            options["source"],
                            owners));
        return;
    }

    if command == "status" {
        send_line(response_username,
                  "I currently have data for the following channels:");
        let mut sorted_channels: Vec<&String> = all_channel_data.keys().collect();
        sorted_channels.sort();
        for channel in sorted_channels {
            let ref channel_data = all_channel_data[channel];
            if let Some(ref topic) = channel_data.current_topic {
                send_line(None,
                          &*format!("  {} ({} lines buffered on \"{}\")",
                                    channel,
                                    topic.lines.len(),
                                    topic.topic));
                match topic.github_url {
                    None => send_line(None, "    no GitHub URL to comment on"),
                    Some(ref github_url) => {
                        send_line(None, &*format!("    will comment on {}", github_url))
                    }
                };
            } else {
                send_line(None, &*format!("  {} (no topic data buffered)", channel));
            }
        }
        return;
    }

    if command == "bye" {
        if response_target.starts_with('#') {
            // FIXME: code duplication
            let this_channel_data = all_channel_data
                .entry(String::from(response_target))
                .or_insert_with(|| ChannelData::new(options));
            this_channel_data.end_topic();
            server.send(Command::PART(String::from(response_target),
                        Some(format!("Leaving at request of {}.  Feel free to /invite me back.",
                                     response_username.unwrap())))).unwrap();
        } else {
            send_line(response_username, "'bye' only works in a channel");
        }
        return;
    }

    send_line(response_username,
              "Sorry, I don't understand that command.  Try 'help'.");
}

struct ChannelLine {
    source: String,
    is_action: bool,
    message: String,
}

struct TopicData {
    topic: String,
    github_url: Option<String>,
    lines: Vec<ChannelLine>,
    resolutions: Vec<String>,
}

struct ChannelData<'opts> {
    current_topic: Option<TopicData>,
    options: &'opts HashMap<String, String>,
}

impl fmt::Display for ChannelLine {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.is_action {
            write!(f, "* {} {}", self.source, self.message)
        } else {
            write!(f, "<{}> {}", self.source, self.message)
        }
    }
}

impl TopicData {
    fn new(topic: &str) -> TopicData {
        let topic_ = String::from(topic);
        TopicData {
            topic: topic_,
            github_url: None,
            lines: vec![],
            resolutions: vec![],
        }
    }
}

impl fmt::Display for TopicData {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.resolutions.len() == 0 {
            try!(write!(f,
                        "The CSS Working Group just discussed {}.\n\n",
                        self.topic));
        } else {
            try!(write!(f,
                        "The CSS Working Group just discussed {}, and agreed to the following resolutions:\n\n",
                        self.topic));
            for resolution in &self.resolutions {
                try!(write!(f, "```\n{}\n```\n\n", resolution));
            }
        }

        try!(write!(f,
                    "The full IRC log of that discussion\n"));
        try!(write!(f, "\n```\n"));
        for line in &self.lines {
            try!(write!(f, "{}\n", line));
        }
        try!(write!(f, "```\n\n"));
        Ok(())
    }
}

/// A case-insensitive version of starts_with.
fn ci_starts_with(s: &str, prefix: &str) -> bool {
    assert!(prefix.to_lowercase() == prefix);

    s.len() >= prefix.len() && s[0..prefix.len()].to_lowercase() == prefix
}

/// Remove a case-insensitive start of the line, and if that prefix is
/// present return the rest of the line.
fn strip_ci_prefix(s: &str, prefix: &str) -> Option<String> {
    if ci_starts_with(s, prefix) {
        Some(String::from(s[prefix.len()..].trim_left()))
    } else {
        None
    }
}

impl<'opts> ChannelData<'opts> {
    fn new(options_: &'opts HashMap<String, String>) -> ChannelData {
        ChannelData {
            current_topic: None,
            options: options_,
        }
    }

    // Returns the response that should be sent to the message over IRC.
    fn add_line(&mut self, line: ChannelLine) -> Option<String> {
        if let Some(ref topic) = strip_ci_prefix(&line.message, "topic:") {
            self.start_topic(topic);
        }
        if line.source == "trackbot" && line.is_action == true &&
           line.message == "is ending a teleconference." {
            self.end_topic();
        }
        match self.current_topic {
            None => None,
            Some(ref mut data) => {
                let new_url_option = extract_github_url(&line.message, self.options);
                let response = match (new_url_option.as_ref(), &data.github_url) {
                    (None, _) => None,
                    (Some(&None), &None) => None,
                    (Some(&None), _) => Some(String::from("OK, I won't post this discussion to GitHub")),
                    (Some(&Some(ref new_url)), &None) => {
                        Some(format!("OK, I'll post this discussion to {}", new_url))
                    }
                    (Some(new_url), old_url) if *old_url == *new_url => None,
                    (Some(&Some(ref new_url)), &Some(ref old_url)) => {
                        Some(format!("OK, I'll post this discussion to {} instead of {} like you said before",
                                     new_url,
                                     old_url))
                    }
                };

                if let Some(new_url) = new_url_option {
                    data.github_url = new_url;
                }

                if line.message.starts_with("RESOLUTION") || line.message.starts_with("RESOLVED") {
                    data.resolutions.push(line.message.clone());
                }

                data.lines.push(line);

                response
            }
        }
    }

    fn start_topic(&mut self, topic: &str) {
        self.end_topic();
        self.current_topic = Some(TopicData::new(topic));
    }

    fn end_topic(&mut self) {
        // TODO: Test the topic boundary code.
        if let Some(topic) = self.current_topic.take() {
            if topic.github_url.is_some() {
                let task = GithubCommentTask::new(topic, self.options);
                task.run();
            }
        }
    }
}

fn extract_github_url(message: &str, options: &HashMap<String, String>) -> Option<Option<String>> {
    lazy_static! {
        static ref GITHUB_URL_RE: Regex =
            Regex::new(r"^https://github.com/(?P<repo>[^/]*/[^/]*)/issues/(?P<number>[0-9]+)$")
            .unwrap();
    }
    let ref allowed_repos = options["github_repos_allowed"];
    for prefix in ["topic:", "github topic:"].into_iter() {
        if let Some(ref maybe_url) = strip_ci_prefix(&message, prefix) {
            if maybe_url.to_lowercase() == "none" {
                return Some(None);
            } else if let Some(ref caps) = GITHUB_URL_RE.captures(maybe_url) {
                for repo in allowed_repos.split_whitespace() {
                    if caps["repo"] == *repo {
                        return Some(Some(maybe_url.clone()));
                    }
                }
            }
        }
    }
    None
}

struct GithubCommentTask {
    data: TopicData,
    github: Github,
}

impl GithubCommentTask {
    fn new(data_: TopicData, options: &HashMap<String, String>) -> GithubCommentTask {
        let github_ =
            Github::new(&*options["github_uastring"],
                        Client::with_connector(HttpsConnector::new(NativeTlsClient::new()
                                                                       .unwrap())),
                        Credentials::Token(options["github_access_token"].clone()));
        GithubCommentTask {
            data: data_,
            github: github_,
        }
    }
    fn run(self) {
        thread::spawn(move || { self.main(); });
    }
    fn main(&self) {
        lazy_static! {
            static ref GITHUB_URL_RE: Regex =
                Regex::new(r"^https://github.com/(?P<owner>[^/]*)/(?P<repo>[^/]*)/issues/(?P<number>[0-9]+)$")
                .unwrap();
        }

        if let Some(ref github_url) = self.data.github_url {
            if let Some(ref caps) = GITHUB_URL_RE.captures(github_url) {
                let repo = self.github
                    .repo(String::from(&caps["owner"]), String::from(&caps["repo"]));
                let issue = repo.issue(caps["number"].parse::<u64>().unwrap());
                let comments = issue.comments();

                let comment_text = format!("{}", self.data);
                comments
                    .create(&CommentOptions { body: comment_text })
                    .unwrap();
            } else {
                warn!("How does {} fail to match now when it matched before?",
                      github_url)
            }
        }
    }
}
<|endoftext|>


<commit_before>#![recursion_limit = "128"]

extern crate ape;
extern crate app_dirs;
extern crate core;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_codegen;
#[macro_use]
extern crate error_chain;
extern crate getopts;
extern crate hyper;
extern crate id3;
extern crate image;
extern crate iron;
extern crate lewton;
extern crate metaflac;
extern crate mount;
extern crate params;
extern crate rand;
extern crate reqwest;
extern crate regex;
extern crate ring;
extern crate router;
extern crate secure_session;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate staticfile;
extern crate toml;
extern crate typemap;
extern crate url;

#[cfg(windows)]
extern crate uuid;
#[cfg(windows)]
extern crate winapi;
#[cfg(windows)]
extern crate kernel32;
#[cfg(windows)]
extern crate shell32;
#[cfg(windows)]
extern crate user32;

#[cfg(unix)]
extern crate unix_daemonize;

#[cfg(unix)]
use unix_daemonize::{daemonize_redirect, ChdirMode};

use core::ops::Deref;
use errors::*;
use getopts::Options;
use iron::prelude::*;
use mount::Mount;
use staticfile::Static;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::channel;

mod api;
mod config;
mod db;
mod ddns;
mod errors;
mod index;
mod metadata;
mod ui;
mod user;
mod utils;
mod thumbnails;
mod vfs;

fn main() {
	if let Err(ref e) = run() {
		println!("Error: {}", e);

		for e in e.iter().skip(1) {
			println!("caused by: {}", e);
		}
		if let Some(backtrace) = e.backtrace() {
			println!("backtrace: {:?}", backtrace);
		}
		::std::process::exit(1);
	}
}

#[cfg(unix)]
fn daemonize() -> Result<()> {
	let mut log_file = utils::get_data_root()?;
	log_file.push("polaris.log");
	match daemonize_redirect(Some(&log_file), Some(&log_file), ChdirMode::NoChdir) {
		Ok(_) => Ok(()),
		Err(_) => bail!(ErrorKind::DaemonError),
	}
}

fn run() -> Result<()> {

	#[cfg(unix)]
	daemonize()?;

	// Parse CLI options
	let args: Vec<String> = std::env::args().collect();
	let mut options = Options::new();
	options.optopt("c", "config", "set the configuration file", "FILE");
	options.optopt("d", "database", "set the path to index database", "FILE");
	options.optopt("w", "web", "set the path to web client files", "DIRECTORY");
	let matches = options.parse(&args[1..])?;

	// Init DB
	println!("Starting up database");
	let db_file_name = matches.opt_str("d").unwrap_or("db.sqlite".to_owned());
	let db_file_path = Path::new(&db_file_name);
	let db = Arc::new(db::DB::new(&db_file_path)?);

	// Parse config
	let config_file_name = matches.opt_str("c");
	let config_file_path = config_file_name.map(|p| Path::new(p.as_str()).to_path_buf());
	if let Some(path) = config_file_path {
		let config = config::parse_toml_file(&path)?;
		config::overwrite(db.deref(), &config)?;
	}

	// Init index
	let (index_sender, index_receiver) = channel();
	let index_sender = Arc::new(Mutex::new(index_sender));
	let db_ref = db.clone();
	std::thread::spawn(move || {
		                   let db = db_ref.deref();
		                   index::update_loop(db, index_receiver);
		                  });

	// Trigger auto-indexing
	let db_ref = db.clone();
	let sender_ref = index_sender.clone();
	std::thread::spawn(move || { index::self_trigger(db_ref.deref(), sender_ref); });

	// Mount API
	println!("Mounting API");
	let mut mount = Mount::new();
	let handler = api::get_handler(db.clone(), index_sender)?;
	mount.mount("/api/", handler);

	// Mount static files
	println!("Mounting static files");
	let web_dir_name = matches.opt_str("w");
	let mut default_web_dir = utils::get_data_root()?;
	default_web_dir.push("web");
	let web_dir_path = web_dir_name
		.map(|n| Path::new(n.as_str()).to_path_buf())
		.unwrap_or(default_web_dir);

	mount.mount("/", Static::new(web_dir_path));

	println!("Starting up server");
	let mut server = match Iron::new(mount).http(("0.0.0.0", 5050)) {
		Ok(s) => s,
		Err(e) => bail!("Error starting up server: {}", e),
	};

	// Start DDNS updates
	let db_ref = db.clone();
	std::thread::spawn(move || { ddns::run(db_ref.deref()); });

	// Run UI
	ui::run();

	println!("Shutting down server");
	if let Err(e) = server.close() {
		bail!("Error shutting down server: {}", e);
	}

	Ok(())
}
<commit_msg>Fixed a bug where default database location was incorrect<commit_after>#![recursion_limit = "128"]

extern crate ape;
extern crate app_dirs;
extern crate core;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_codegen;
#[macro_use]
extern crate error_chain;
extern crate getopts;
extern crate hyper;
extern crate id3;
extern crate image;
extern crate iron;
extern crate lewton;
extern crate metaflac;
extern crate mount;
extern crate params;
extern crate rand;
extern crate reqwest;
extern crate regex;
extern crate ring;
extern crate router;
extern crate secure_session;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate staticfile;
extern crate toml;
extern crate typemap;
extern crate url;

#[cfg(windows)]
extern crate uuid;
#[cfg(windows)]
extern crate winapi;
#[cfg(windows)]
extern crate kernel32;
#[cfg(windows)]
extern crate shell32;
#[cfg(windows)]
extern crate user32;

#[cfg(unix)]
extern crate unix_daemonize;

#[cfg(unix)]
use unix_daemonize::{daemonize_redirect, ChdirMode};

use core::ops::Deref;
use errors::*;
use getopts::Options;
use iron::prelude::*;
use mount::Mount;
use staticfile::Static;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::channel;

mod api;
mod config;
mod db;
mod ddns;
mod errors;
mod index;
mod metadata;
mod ui;
mod user;
mod utils;
mod thumbnails;
mod vfs;

fn main() {
	if let Err(ref e) = run() {
		println!("Error: {}", e);

		for e in e.iter().skip(1) {
			println!("caused by: {}", e);
		}
		if let Some(backtrace) = e.backtrace() {
			println!("backtrace: {:?}", backtrace);
		}
		::std::process::exit(1);
	}
}

#[cfg(unix)]
fn daemonize() -> Result<()> {
	let mut log_file = utils::get_data_root()?;
	log_file.push("polaris.log");
	match daemonize_redirect(Some(&log_file), Some(&log_file), ChdirMode::NoChdir) {
		Ok(_) => Ok(()),
		Err(_) => bail!(ErrorKind::DaemonError),
	}
}

fn run() -> Result<()> {

	#[cfg(unix)]
	daemonize()?;

	// Parse CLI options
	let args: Vec<String> = std::env::args().collect();
	let mut options = Options::new();
	options.optopt("c", "config", "set the configuration file", "FILE");
	options.optopt("d", "database", "set the path to index database", "FILE");
	options.optopt("w", "web", "set the path to web client files", "DIRECTORY");
	let matches = options.parse(&args[1..])?;

	// Init DB
	println!("Starting up database");
	let db_path = matches.opt_str("d");
	let mut default_db_path = utils::get_data_root()?;
	default_db_path.push("db.sqlite");
	let db_path = db_path
		.map(|n| Path::new(n.as_str()).to_path_buf())
		.unwrap_or(default_db_path);
	let db = Arc::new(db::DB::new(&db_path)?);

	// Parse config
	let config_file_name = matches.opt_str("c");
	let config_file_path = config_file_name.map(|p| Path::new(p.as_str()).to_path_buf());
	if let Some(path) = config_file_path {
		let config = config::parse_toml_file(&path)?;
		config::overwrite(db.deref(), &config)?;
	}

	// Init index
	let (index_sender, index_receiver) = channel();
	let index_sender = Arc::new(Mutex::new(index_sender));
	let db_ref = db.clone();
	std::thread::spawn(move || {
		                   let db = db_ref.deref();
		                   index::update_loop(db, index_receiver);
		                  });

	// Trigger auto-indexing
	let db_ref = db.clone();
	let sender_ref = index_sender.clone();
	std::thread::spawn(move || { index::self_trigger(db_ref.deref(), sender_ref); });

	// Mount API
	println!("Mounting API");
	let mut mount = Mount::new();
	let handler = api::get_handler(db.clone(), index_sender)?;
	mount.mount("/api/", handler);

	// Mount static files
	println!("Mounting static files");
	let web_dir_name = matches.opt_str("w");
	let mut default_web_dir = utils::get_data_root()?;
	default_web_dir.push("web");
	let web_dir_path = web_dir_name
		.map(|n| Path::new(n.as_str()).to_path_buf())
		.unwrap_or(default_web_dir);

	mount.mount("/", Static::new(web_dir_path));

	println!("Starting up server");
	let mut server = match Iron::new(mount).http(("0.0.0.0", 5050)) {
		Ok(s) => s,
		Err(e) => bail!("Error starting up server: {}", e),
	};

	// Start DDNS updates
	let db_ref = db.clone();
	std::thread::spawn(move || { ddns::run(db_ref.deref()); });

	// Run UI
	ui::run();

	println!("Shutting down server");
	if let Err(e) = server.close() {
		bail!("Error shutting down server: {}", e);
	}

	Ok(())
}
<|endoftext|>


<commit_before>#![recursion_limit = "1024"]

#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate log;
#[macro_use]
extern crate nom;

extern crate bip_bencode;
extern crate bip_metainfo;
extern crate bip_utracker;
extern crate bip_util;
extern crate chrono;
extern crate hyper;
extern crate pnet;
extern crate pnet_macros_support;
extern crate pretty_env_logger;
extern crate rustyline;
extern crate url;

mod btclient;
mod packet;

mod errors {
    // Create the Error, ErrorKind, ResultExt, and Result types
    error_chain!{}
}

use bip_bencode::Bencode;
use bip_metainfo::MetainfoFile;
use bip_utracker::contact::CompactPeersV4;
use errors::*;
use chrono::{TimeZone, UTC};
use hyper::Client;
use hyper::header::Connection;
use packet::peer_pkt::{MutablePeerHandshakePacket, MutablePeerMessagePacket};
use pnet::packet::Packet;
use rustyline::Editor;
use rustyline::error::ReadlineError;
use url::Url;

use std::fs::File;
use std::io::prelude::*;
use std::net::TcpStream;
use std::str;
use std::string::String;
use std::time::{SystemTime, UNIX_EPOCH};
use std::thread;

const HISTORY_FILE: &'static str = ".rustyline.history";
const PEER_HANDSHAKE_STRUCT_SZ: usize = 68;
const PEER_REQ_PKT_SZ: usize = 14;

/// Print file list from the torrent.
fn print_files(bytes: &[u8]) {
    let metainfo = MetainfoFile::from_bytes(bytes).unwrap();
    let info = metainfo.info();

    println!("File List:");
    println!("Size (bytes)\tPath");
    println!("------------\t----------------------------------------------");
    for file in info.files() {
        println!("{:12}\t{}",
                 file.length(),
                 file.paths().next().unwrap_or("<unknown>"));
    }
}

/// Print general information about the torrent.
fn print_metainfo_overview(bytes: &[u8]) {
    let metainfo = MetainfoFile::from_bytes(bytes).unwrap();
    let info = metainfo.info();
    let info_hash_hex = metainfo.info_hash()
        .as_ref()
        .iter()
        .map(|b| format!("{:02x}", b))
        .fold(String::new(), |mut acc, nex| {
            acc.push_str(&nex);
            acc
        });
    let utc_creation_date = metainfo.creation_date().map(|c| UTC.timestamp(c, 0));

    println!("------Metainfo File Overview-----");

    println!("InfoHash: {}", info_hash_hex);
    println!("Main Tracker: {:?}", metainfo.main_tracker());
    println!("Comment: {:?}", metainfo.comment());
    println!("Creator: {:?}", metainfo.created_by());
    println!("Creation Date: {:?}", utc_creation_date);

    println!("Directory: {:?}", info.directory());
    println!("Piece Length: {:?}", info.piece_length());
    println!("Number Of Pieces: {}", info.pieces().count());
    println!("Number Of Files: {}", info.files().count());
    println!("Total File Size: {}\n",
             info.files().fold(0, |acc, nex| acc + nex.length()));

    print_files(bytes);
}

fn connect_to_tracker(metainfo: MetainfoFile,
                      peer_id: &str,
                      port: u16)
                      -> Result<(Vec<String>, String), String> {
    debug!("connecting to tracker: {:?}", metainfo.main_tracker());

    let info_hash = metainfo.info_hash();
    // TODO figure can this conversion to url-encoded form be done safely?
    let info_hash_str = unsafe { str::from_utf8_unchecked(info_hash.as_ref()) };

    // TODO this needs to be calculated based on what we have
    let total_len = metainfo.info().files().fold(0, |acc, nex| acc + nex.length());

    let mut url = Url::parse(metainfo.main_tracker().unwrap()).unwrap();
    url.query_pairs_mut()
        .append_pair("info_hash", info_hash_str)
        .append_pair("peer_id", peer_id)
        .append_pair("port", &(port.to_string()))
        // TODO parametrize this
        .append_pair("uploaded", "0")
        // TODO parametrize this
        .append_pair("downloaded", "0")
        // TODO see note on total_len above
        .append_pair("left", &(total_len.to_string()))
        .append_pair("compact", "1")
        .append_pair("event", "started")
        .append_pair("supportcrypto", "0");
    trace!("URL {:?}", url);

    let client = Client::new();
    let mut res = client.get(url).header(Connection::close()).send().unwrap();
    let mut buffer = Vec::new();
    res.read_to_end(&mut buffer).unwrap();
    debug!("{:?}", res);
    let bencode = Bencode::decode(&buffer).unwrap();
    trace!("{:?}", bencode);
    let (_, peers) = CompactPeersV4::from_bytes(bencode.dict()
            .unwrap()
            .lookup("peers")
            .unwrap()
            .bytes()
            .unwrap())
        .unwrap();
    trace!("{:?}", peers);
    let mut ip_ports: Vec<String> = Vec::new();
    debug!("Peer list received:");
    for peer in peers.iter() {
        debug!("{:?}", peer);
        ip_ports.push(peer.to_string());
    }
    Ok((ip_ports, info_hash_str.to_string()))
}

fn peer_connections(peer_ip_ports: Vec<String>,
                    info_hash: &str,
                    peer_id: &str)
                    -> Result<(), String> {
    for peer_ip_port in peer_ip_ports {
        let peer_ip_port_cl = peer_ip_port.clone();
        let info_hash_cl = info_hash.to_string().clone();
        let peer_id_cl = peer_id.to_string().clone();
        thread::spawn(move || {
            match handshake_peer(&peer_ip_port_cl, &info_hash_cl, &peer_id_cl) {
                Ok(mut stream) => {
                    debug!("Communicating with peer {:?}", peer_ip_port_cl);
                    let mut buff = [0; PEER_HANDSHAKE_STRUCT_SZ];
                    match stream.read(&mut buff) {
                        Ok(_) => {
                            // TODO determine the below three parameters logically
                            let index = 0;
                            let begin = 0;
                            let length = 10;
                            match request_piece(stream, index, begin, length) {
                                Ok(buf_read) => {
                                    debug!("buf_read: {:?}", buf_read);
                                }
                                Err(_) => error!("Requesting piece failed!"),
                            }
                        }
                        Err(e) => error!("Reading from the stream failed! {:?}", e),
                    }
                }
                Err(_) => {
                    error!("Closing thread with peer {:?}", peer_ip_port_cl);
                }
            }
        });
    }
    Ok(())
}

fn request_piece(mut stream: TcpStream,
                 index: u32,
                 begin: u32,
                 length: u32)
                 -> Result<Vec<u8>, String> {
    let mut buf = vec![0u8; PEER_REQ_PKT_SZ];
    let mut peer_msg_pkt = MutablePeerMessagePacket::new(&mut buf).unwrap();
    peer_msg_pkt.set_len(13);
    peer_msg_pkt.set_id(6);
    let mut index_begin_length: Vec<u32> = Vec::new();
    index_begin_length.push(index);
    index_begin_length.push(begin);
    index_begin_length.push(length);
    let byte_arr = unsafe { std::mem::transmute::<Vec<u32>, Vec<u8>>(index_begin_length) };

    peer_msg_pkt.set_payload(&byte_arr);

    match stream.write(peer_msg_pkt.packet()) {
        Ok(_) => {
            let mut buf_read = vec![0; 2048];
            match stream.read(&mut buf_read) {
                Ok(bytes_read) => {
                    debug!("Bytes read: {:?}", bytes_read);
                    Ok(buf_read[..bytes_read].to_vec())
                }
                Err(e) => {
                    error!("Read failed! {:?}", e);
                    Err("Read failed!".to_owned())
                }
            }
        }
        Err(e) => {
            error!("Write to stream failed! {:?}", e);
            Err("Write to stream failed!".to_owned())
        }
    }
}

fn handshake_peer(peer_ip_port: &str, info_hash: &str, peer_id: &str) -> Result<TcpStream, String> {
    let mut buf = vec![0u8; PEER_HANDSHAKE_STRUCT_SZ];
    let mut ph = MutablePeerHandshakePacket::new(&mut buf).unwrap();
    ph.set_pstrlen("BitTorrent protocol".len() as u8);
    ph.set_pstr(&String::from("BitTorrent protocol").into_bytes());
    ph.set_reserved(&[0; 8]);
    ph.set_info_hash(info_hash.as_bytes());
    ph.set_peer_id(peer_id.as_bytes());
    match TcpStream::connect(peer_ip_port) {
        Ok(mut stream) => {
            debug!("Connection to peer {:?} successful!", peer_ip_port);
            match stream.write(ph.packet()) {
                Ok(_) => {
                    debug!("Sending message successful!");
                    trace!("{:?}", ph.packet());
                    Ok((stream))
                }
                Err(_) => {
                    error!("Sending message failed!");
                    Err("Sending message failed!".to_owned())
                }
            }
        }
        Err(e) => {
            error!("Connection to peer {:?} failed! {:?}", peer_ip_port, e);
            Err("Connection to peer failed!".to_owned())
        }
    }
}

fn main() {
    pretty_env_logger::init();

    let mut rl = Editor::<()>::new();
    if rl.load_history(HISTORY_FILE).is_err() {
        info!("No previous history!");
    }

    let now = SystemTime::now();
    let duration = now.duration_since(UNIX_EPOCH).unwrap();
    let client_id = "-bittorrent-rs-".to_owned() + &format!("{}", duration.as_secs() % 100_000);

    loop {
        let readline = rl.readline("> ");
        match readline {
            Ok(cmd) => {
                rl.add_history_entry(&cmd);

                let cmd = cmd.trim().split(' ').collect::<Vec<&str>>();

                match cmd[0] {
                    "help" | "h" => {
                        println!("Commands:
parse/p <torrent file path>      - show Metainfo File Overview
connect/c <torrent file path>    - initiate connection to tracker, and handshake with peers
showfiles/sf <torrent file path> - show files in the torrent
help/h                           - show this help");
                    }
                    "parse" | "p" => {
                        if cmd.len() != 2 {
                            error!("usage: parse <torrent file>");
                        } else {
                            let path = cmd[1];
                            debug!("MetainfoFile: {:?}", path);

                            match File::open(path) {
                                Ok(mut f) => {
                                    let mut bytes: Vec<u8> = Vec::new();
                                    f.read_to_end(&mut bytes).unwrap();
                                    print_metainfo_overview(&bytes);
                                }
                                Err(e) => error!("{:?}", e),
                            }
                        }
                    }
                    "connect" | "c" => {
                        if cmd.len() != 2 {
                            error!("usage: connect <torrent file>");
                        } else {
                            let path = cmd[1];
                            match File::open(path) {
                                Ok(mut f) => {
                                    let mut bytes: Vec<u8> = Vec::new();
                                    f.read_to_end(&mut bytes).unwrap();

                                    // TODO: generate peer ID
                                    let result =
                                        connect_to_tracker(MetainfoFile::from_bytes(&bytes)
                                                               .unwrap(),
                                                           &client_id,
                                                           6882)
                                            .unwrap();
                                    peer_connections(result.0, &result.1, &client_id).unwrap();
                                }
                                Err(e) => error!("{:?}", e),
                            }
                        }
                    }
                    "showfiles" | "sf" => {
                        if cmd.len() != 2 {
                            error!("usage: showfiles <torrent file>");
                        } else {
                            let path = cmd[1];
                            match File::open(path) {
                                Ok(mut f) => {
                                    let mut bytes: Vec<u8> = Vec::new();
                                    f.read_to_end(&mut bytes).unwrap();

                                    print_files(&bytes);
                                }
                                Err(e) => error!("{:?}", e),
                            }
                        }
                    }
                    "" => {}
                    _ => {
                        println!("invalid command, see \"help\"");
                    }
                }
            }
            Err(ReadlineError::Interrupted) |
            Err(ReadlineError::Eof) => {
                break;
            }
            Err(err) => {
                error!("Error: {:?}", err);
                break;
            }
        }
    }
    rl.save_history(HISTORY_FILE).unwrap();
}
<commit_msg>Refactor to use error_chain<commit_after>#![recursion_limit = "1024"]

#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate log;
#[macro_use]
extern crate nom;

extern crate bip_bencode;
extern crate bip_metainfo;
extern crate bip_utracker;
extern crate bip_util;
extern crate chrono;
extern crate hyper;
extern crate pnet;
extern crate pnet_macros_support;
extern crate pretty_env_logger;
extern crate rustyline;
extern crate url;

mod btclient;
mod packet;

mod errors {
    // Create the Error, ErrorKind, ResultExt, and Result types
    error_chain!{}
}

use bip_bencode::Bencode;
use bip_metainfo::MetainfoFile;
use bip_utracker::contact::CompactPeersV4;
use errors::*;
use chrono::{TimeZone, UTC};
use hyper::Client;
use hyper::header::Connection;
use packet::peer_pkt::{MutablePeerHandshakePacket, MutablePeerMessagePacket};
use pnet::packet::Packet;
use rustyline::Editor;
use rustyline::error::ReadlineError;
use url::Url;

use std::fs::File;
use std::io::prelude::*;
use std::net::TcpStream;
use std::str;
use std::string::String;
use std::time::{SystemTime, UNIX_EPOCH};
use std::thread;

const HISTORY_FILE: &'static str = ".rustyline.history";
const PEER_HANDSHAKE_STRUCT_SZ: usize = 68;
const PEER_REQ_PKT_SZ: usize = 14;

/// Print file list from the torrent.
fn print_files(bytes: &[u8]) -> Result<()> {
    let metainfo = MetainfoFile::from_bytes(bytes).unwrap();
    let info = metainfo.info();

    println!("File List:");
    println!("Size (bytes)\tPath");
    println!("------------\t----------------------------------------------");
    for file in info.files() {
        println!("{:12}\t{}",
                 file.length(),
                 file.paths().next().unwrap_or("<unknown>"));
    }
    Ok(())
}

/// Print general information about the torrent.
fn print_metainfo_overview(bytes: &[u8]) -> Result<()> {
    let metainfo = MetainfoFile::from_bytes(bytes).unwrap();
    let info = metainfo.info();
    let info_hash_hex = metainfo.info_hash()
        .as_ref()
        .iter()
        .map(|b| format!("{:02x}", b))
        .fold(String::new(), |mut acc, nex| {
            acc.push_str(&nex);
            acc
        });
    let utc_creation_date = metainfo.creation_date().map(|c| UTC.timestamp(c, 0));

    println!("------Metainfo File Overview-----");

    println!("InfoHash: {}", info_hash_hex);
    println!("Main Tracker: {:?}", metainfo.main_tracker());
    println!("Comment: {:?}", metainfo.comment());
    println!("Creator: {:?}", metainfo.created_by());
    println!("Creation Date: {:?}", utc_creation_date);

    println!("Directory: {:?}", info.directory());
    println!("Piece Length: {:?}", info.piece_length());
    println!("Number Of Pieces: {}", info.pieces().count());
    println!("Number Of Files: {}", info.files().count());
    println!("Total File Size: {}\n",
             info.files().fold(0, |acc, nex| acc + nex.length()));

    print_files(bytes)?;
    Ok(())
}

fn connect_to_tracker(metainfo: MetainfoFile,
                      peer_id: &str,
                      port: u16)
                      -> Result<(Vec<String>, String)> {
    debug!("connecting to tracker: {:?}", metainfo.main_tracker());

    let info_hash = metainfo.info_hash();
    // TODO figure can this conversion to url-encoded form be done safely?
    let info_hash_str = unsafe { str::from_utf8_unchecked(info_hash.as_ref()) };

    // TODO this needs to be calculated based on what we have
    let total_len = metainfo.info().files().fold(0, |acc, nex| acc + nex.length());

    let mut url = Url::parse(metainfo.main_tracker().unwrap()).unwrap();
    url.query_pairs_mut()
        .append_pair("info_hash", info_hash_str)
        .append_pair("peer_id", peer_id)
        .append_pair("port", &(port.to_string()))
        // TODO parametrize this
        .append_pair("uploaded", "0")
        // TODO parametrize this
        .append_pair("downloaded", "0")
        // TODO see note on total_len above
        .append_pair("left", &(total_len.to_string()))
        .append_pair("compact", "1")
        .append_pair("event", "started")
        .append_pair("supportcrypto", "0");
    trace!("URL {:?}", url);

    let client = Client::new();
    let mut res = client.get(url).header(Connection::close()).send().unwrap();
    let mut buffer = Vec::new();
    res.read_to_end(&mut buffer).unwrap();
    debug!("{:?}", res);
    let bencode = Bencode::decode(&buffer).unwrap();
    trace!("{:?}", bencode);
    let (_, peers) = CompactPeersV4::from_bytes(bencode.dict()
            .unwrap()
            .lookup("peers")
            .unwrap()
            .bytes()
            .unwrap())
        .unwrap();
    trace!("{:?}", peers);
    let mut ip_ports: Vec<String> = Vec::new();
    debug!("Peer list received:");
    for peer in peers.iter() {
        debug!("{:?}", peer);
        ip_ports.push(peer.to_string());
    }
    Ok((ip_ports, info_hash_str.to_string()))
}

fn peer_connections(peer_ip_ports: Vec<String>, info_hash: &str, peer_id: &str) -> Result<()> {
    for peer_ip_port in peer_ip_ports {
        let peer_ip_port_cl = peer_ip_port.clone();
        let info_hash_cl = info_hash.to_string().clone();
        let peer_id_cl = peer_id.to_string().clone();
        thread::spawn(move || {
            match handshake_peer(&peer_ip_port_cl, &info_hash_cl, &peer_id_cl) {
                Ok(mut stream) => {
                    debug!("Communicating with peer {:?}", peer_ip_port_cl);
                    let mut buff = [0; PEER_HANDSHAKE_STRUCT_SZ];
                    match stream.read(&mut buff) {
                        Ok(_) => {
                            // TODO determine the below three parameters logically
                            let index = 0;
                            let begin = 0;
                            let length = 10;
                            match request_piece(stream, index, begin, length) {
                                Ok(buf_read) => {
                                    debug!("buf_read: {:?}", buf_read);
                                }
                                Err(_) => error!("Requesting piece failed!"),
                            }
                        }
                        Err(e) => error!("Reading from the stream failed! {:?}", e),
                    }
                }
                Err(_) => {
                    error!("Closing thread with peer {:?}", peer_ip_port_cl);
                }
            }
        });
    }
    Ok(())
}

fn request_piece(mut stream: TcpStream, index: u32, begin: u32, length: u32) -> Result<Vec<u8>> {
    let mut buf = vec![0u8; PEER_REQ_PKT_SZ];
    let mut peer_msg_pkt = MutablePeerMessagePacket::new(&mut buf).unwrap();
    peer_msg_pkt.set_len(13);
    peer_msg_pkt.set_id(6);
    let mut index_begin_length: Vec<u32> = Vec::new();
    index_begin_length.push(index);
    index_begin_length.push(begin);
    index_begin_length.push(length);
    let byte_arr = unsafe { std::mem::transmute::<Vec<u32>, Vec<u8>>(index_begin_length) };

    peer_msg_pkt.set_payload(&byte_arr);

    match stream.write(peer_msg_pkt.packet()) {
        Ok(_) => {
            let mut buf_read = vec![0; 2048];
            match stream.read(&mut buf_read) {
                Ok(bytes_read) => {
                    debug!("Bytes read: {:?}", bytes_read);
                    Ok(buf_read[..bytes_read].to_vec())
                }
                Err(e) => {
                    // error!("Read failed! {:?}", e);
                    bail!("Read failed! {:?}", e)
                }
            }
        }
        Err(e) => {
            bail!("Write to stream failed! {:?}", e)
            // Err("Write to stream failed!".to_owned())
        }
    }
}

fn handshake_peer(peer_ip_port: &str, info_hash: &str, peer_id: &str) -> Result<TcpStream> {
    let mut buf = vec![0u8; PEER_HANDSHAKE_STRUCT_SZ];
    let mut ph = MutablePeerHandshakePacket::new(&mut buf).unwrap();
    ph.set_pstrlen("BitTorrent protocol".len() as u8);
    ph.set_pstr(&String::from("BitTorrent protocol").into_bytes());
    ph.set_reserved(&[0; 8]);
    ph.set_info_hash(info_hash.as_bytes());
    ph.set_peer_id(peer_id.as_bytes());
    match TcpStream::connect(peer_ip_port) {
        Ok(mut stream) => {
            debug!("Connection to peer {:?} successful!", peer_ip_port);
            match stream.write(ph.packet()) {
                Ok(_) => {
                    debug!("Sending message successful!");
                    trace!("{:?}", ph.packet());
                    Ok((stream))
                }
                Err(_) => {
                    bail!("Sending message failed!")
                    // Err("Sending message failed!".to_owned())
                }
            }
        }
        Err(e) => {
            bail!("Connection to peer {:?} failed! {:?}", peer_ip_port, e)
            // Err("Connection to peer failed!".to_owned())
        }
    }
}

fn main() {
    pretty_env_logger::init();

    // See: https://brson.github.io/2016/11/30/starting-with-error-chain for explanation
    if let Err(ref e) = run() {
        println!("error: {}", e);

        for e in e.iter().skip(1) {
            println!("caused by: {}", e);
        }

        // The backtrace is not always generated. Try to run this example
        // with `RUST_BACKTRACE=1`.
        if let Some(backtrace) = e.backtrace() {
            println!("backtrace: {:?}", backtrace);
        }

        ::std::process::exit(1);
    }
}

fn run() -> Result<()> {
    let mut rl = Editor::<()>::new();
    if rl.load_history(HISTORY_FILE).is_err() {
        info!("No previous history!");
    }

    let now = SystemTime::now();
    let duration = now.duration_since(UNIX_EPOCH).unwrap();
    let client_id = "-bittorrent-rs-".to_owned() + &format!("{}", duration.as_secs() % 100_000);

    loop {
        let readline = rl.readline("> ");
        match readline {
            Ok(cmd) => {
                rl.add_history_entry(&cmd);

                let cmd = cmd.trim().split(' ').collect::<Vec<&str>>();

                match cmd[0] {
                    "help" | "h" => {
                        println!("Commands:
parse/p <torrent file path>      - show Metainfo File Overview
connect/c <torrent file path>    - initiate connection to tracker, and handshake with peers
showfiles/sf <torrent file path> - show files in the torrent
help/h                           - show this help");
                    }
                    "parse" | "p" => {
                        if cmd.len() != 2 {
                            error!("usage: parse <torrent file>");
                        } else {
                            let path = cmd[1];
                            debug!("MetainfoFile: {:?}", path);

                            match File::open(path) {
                                Ok(mut f) => {
                                    let mut bytes: Vec<u8> = Vec::new();
                                    f.read_to_end(&mut bytes).unwrap();
                                    print_metainfo_overview(&bytes)?;
                                }
                                Err(e) => error!("{:?}", e),
                            }
                        }
                    }
                    "connect" | "c" => {
                        if cmd.len() != 2 {
                            error!("usage: connect <torrent file>");
                        } else {
                            let path = cmd[1];
                            match File::open(path) {
                                Ok(mut f) => {
                                    let mut bytes: Vec<u8> = Vec::new();
                                    f.read_to_end(&mut bytes).unwrap();

                                    // TODO: generate peer ID
                                    let result =
                                        connect_to_tracker(MetainfoFile::from_bytes(&bytes)
                                                               .unwrap(),
                                                           &client_id,
                                                           6882)?;
                                    // .unwrap();
                                    peer_connections(result.0, &result.1, &client_id)?;
                                }
                                Err(e) => error!("{:?}", e),
                            }
                        }
                    }
                    "showfiles" | "sf" => {
                        if cmd.len() != 2 {
                            error!("usage: showfiles <torrent file>");
                        } else {
                            let path = cmd[1];
                            match File::open(path) {
                                Ok(mut f) => {
                                    let mut bytes: Vec<u8> = Vec::new();
                                    f.read_to_end(&mut bytes).unwrap();
                                    print_files(&bytes)?;
                                }
                                Err(e) => error!("{:?}", e),
                            }
                        }
                    }
                    "" => {}
                    _ => {
                        println!("invalid command, see \"help\"");
                    }
                }
            }
            Err(ReadlineError::Interrupted) |
            Err(ReadlineError::Eof) => {
                break;
            }
            Err(err) => {
                error!("Error: {:?}", err);
                break;
            }
        }
    }
    rl.save_history(HISTORY_FILE).unwrap();
    Ok(())
}
<|endoftext|>