irc-1.0.0/.cargo_vcs_info.json0000644000000001360000000000100116070ustar { "git": { "sha1": "b45c5fa88daa21faac7ce00db2cf78d9d228f734" }, "path_in_vcs": "" }irc-1.0.0/.github/workflows/ci.yml000064400000000000000000000022461046102023000151160ustar 00000000000000name: CI on: pull_request: {} push: branches: - develop jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: rust: ["1.67", stable] steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{matrix.rust}} - run: cargo build --workspace --all-targets - run: cargo build --workspace --all-targets --no-default-features - run: cargo build --workspace --all-targets --features tls-native - run: cargo build --workspace --all-targets --features tls-rust # runs all tests for all targets, including examples and benchmarks. Only on # stable, since we don't care about tests running on MSRV. - run: cargo test --workspace --all-targets if: matrix.rust == 'stable' # runs all documentation tests separately, since those are not picked up by # `--all-targets`. - run: cargo test --workspace --doc if: matrix.rust == 'stable' rustfmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - run: cargo fmt --all --check irc-1.0.0/.gitignore000064400000000000000000000001101046102023000123570ustar 00000000000000/target /irc-proto/target /Cargo.lock /irc-proto/Cargo.lock !Cargo.toml irc-1.0.0/.travis.yml000064400000000000000000000031071046102023000125110ustar 00000000000000language: rust sudo: false jobs: allow_failures: - stage: test rust: beta env: FEATURES=default - stage: test rust: nightly env: FEATURES=default include: # Default features - &test rust: stable env: FEATURES=default script: - cargo test --all --no-default-features --features "$FEATURES" - cargo build --no-default-features --features "$FEATURES" - cargo run --no-default-features --features "$FEATURES" --example build-bot # No features - <<: *test env: FEATURES= # CTCP on without toml - <<: *test env: FEATURES=ctcp # CTCP on w/toml (this is the same as default) #- <<: *test # env: FEATURES=ctcp toml_config # nochanlists on without toml - <<: *test env: FEATURES=nochanlists # nochanlists on w/toml - <<: *test env: FEATURES=nochanlists toml_config # tls-rust: disabled until testing server supports TLS 1.3 # - <<: *test # env: FEATURES=tls-rust # Beta - <<: *test rust: beta env: FEATURES=default # Nightly - <<: *test rust: nightly env: FEATURES=default # No idea how to fix this, since we don't depend on futures-preview directly. # - rustdoc --test README.md --extern irc=target/debug/libirc.rlib -L target/debug/deps --edition 2018 notifications: email: false irc: on_failure: always on_success: never channels: - "ircs://irc.pdgn.co:6697/#commits" template: - "%{repository_slug}/%{branch} (%{commit} - %{author}): %{message}" skip_join: true irc-1.0.0/CODE_OF_CONDUCT.md000064400000000000000000000064631046102023000132070ustar 00000000000000# Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. In general, non-maintainer contributors will not be considered representing the project in public spaces except when explicitly stating such. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the lead project maintainer at [aweiss@hey.com](mailto:aweiss@hey.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org irc-1.0.0/CONTRIBUTING.md000064400000000000000000000051431046102023000126330ustar 00000000000000# How to contribute Your contributions are essential for making the irc crate the best piece of software it could possibly be! We welcome contributions from programmers of all skill levels, and are happy to provide mentorship for new contributors working on bug fixes. To make the onboarding process as painless as possible for everyone, there are a few guidelines that we need contributors to follow. ## Getting Started * Make sure you have a [GitHub account](https://github.com/signup/free). * Fork the repository on GitHub. ## Finding Something to Do There will almost certainly be issues open on the irc crate at all times. These are a good place to start if you don't have anything pressing on your mind that you'd like to see. If you need or would like mentoring instructions on any issue, please ping [@aatxe](https://github.com/aatxe), and he will do his best to provide them in a timely fashion. If instead you have your own idea and would like mentoring or feedback, you should open a new issue and mention such a desire. ## Making Changes * Create a topic branch from where you want to base your work. * This is almost always the develop branch. * Only target stable or a maintainer's feature branch if you are certain your fix must be on that branch. * To quickly create a topic branch based on develop, run `git checkout -b fix/develop/my_contribution develop`. Please avoid working directly on the `stable` branch, and keep direct work on `develop` minor. * Make commits of logical and atomic units. * Check for unnecessary whitespace with `git diff --check` before committing. * Make sure you have added the necessary tests for your changes. * Run _all_ the tests to assure nothing else was accidentally broken using `cargo test`. You can also run `cargo test --no-default-features`, but this is generally not relevant and will be handled by our continuous integration tools. * Some tests related to configurations will fail if you don't run the `mktestconfig.sh` script. ## Submitting Changes * Push your changes to a topic branch in your fork of the repository. * Submit a pull request to the `aatxe/irc` repository. * Await maintainer feedback and continuous integration results. * We make an effort to triage quickly, but patience is appreciated! ## Additional Resources * [Code of Conduct](https://github.com/aatxe/irc/blob/develop/CODE_OF_CONDUCT.md) * [the irc crate docs](https://docs.rs/irc) * #rust-irc IRC channel on irc.mozilla.org ## Credits These contributing guidelines were inspired by the [Puppet contributor guidelines](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md). irc-1.0.0/Cargo.lock0000644000001167010000000000100075700ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "anyhow" version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "args" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4b7432c65177b8d5c032d56e020dd8d407e939468479fc8c300e2d93e6d970b" dependencies = [ "getopts", "log", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "windows-targets 0.52.4", ] [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encoding" version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" dependencies = [ "encoding-index-japanese", "encoding-index-korean", "encoding-index-simpchinese", "encoding-index-singlebyte", "encoding-index-tradchinese", ] [[package]] name = "encoding-index-japanese" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" dependencies = [ "encoding_index_tests", ] [[package]] name = "encoding-index-korean" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" dependencies = [ "encoding_index_tests", ] [[package]] name = "encoding-index-simpchinese" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" dependencies = [ "encoding_index_tests", ] [[package]] name = "encoding-index-singlebyte" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" dependencies = [ "encoding_index_tests", ] [[package]] name = "encoding-index-tradchinese" version = "1.20141219.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" dependencies = [ "encoding_index_tests", ] [[package]] name = "encoding_index_tests" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "env_filter" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getopts" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "indexmap" version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "irc" version = "1.0.0" dependencies = [ "anyhow", "args", "chrono", "encoding", "env_logger", "futures", "futures-util", "getopts", "irc-proto", "log", "native-tls", "parking_lot", "pin-project", "rustls-pemfile", "serde", "serde_derive", "serde_json", "serde_yaml", "thiserror", "tokio", "tokio-native-tls", "tokio-rustls", "tokio-socks", "tokio-stream", "tokio-util", "toml", "webpki-roots", ] [[package]] name = "irc-proto" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b5c7fb0c03989e8b31de1c91d0f625057887677e387448e7fc10a6afd4d9e1" dependencies = [ "bytes", "encoding", "thiserror", "tokio", "tokio-util", ] [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "native-tls" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.48.5", ] [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "proc-macro2" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", "once_cell", "spin 0.5.2", "untrusted 0.7.1", "web-sys", "winapi", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring 0.17.8", "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64", ] [[package]] name = "rustls-webpki" version = "0.100.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" dependencies = [ "ring 0.16.20", "untrusted 0.7.1", ] [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring 0.17.8", "untrusted 0.9.0", ] [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring 0.17.8", "untrusted 0.9.0", ] [[package]] name = "security-framework" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] [[package]] name = "serde_yaml" version = "0.9.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" dependencies = [ "indexmap", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "syn" version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio" version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-socks" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" dependencies = [ "either", "futures-util", "thiserror", "tokio", ] [[package]] name = "tokio-stream" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-util" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "toml" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-roots" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ "rustls-webpki 0.100.3", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] irc-1.0.0/Cargo.toml0000644000000071440000000000100076130ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" rust-version = "1.67" name = "irc" version = "1.0.0" authors = ["Aaron Weiss "] description = "the irc crate – usable, async IRC for Rust " documentation = "https://docs.rs/irc/" readme = "README.md" keywords = [ "irc", "client", "thread-safe", "async", "tokio", ] categories = [ "asynchronous", "network-programming", ] license = "MPL-2.0" repository = "https://github.com/aatxe/irc" [[example]] name = "simple_proxy" path = "examples/simple_proxy.rs" required-features = ["proxy"] [[example]] name = "simple_plaintext" path = "examples/simple_plaintext.rs" required-features = ["tls-native"] [dependencies.chrono] version = "0.4.24" features = [ "clock", "std", ] default-features = false [dependencies.encoding] version = "0.2.33" [dependencies.futures-util] version = "0.3.30" features = [ "alloc", "sink", ] default-features = false [dependencies.irc-proto] version = "1.0.0" [dependencies.log] version = "0.4.21" [dependencies.native-tls] version = "0.2.11" optional = true [dependencies.parking_lot] version = "0.12.1" [dependencies.pin-project] version = "1.0.12" [dependencies.rustls-pemfile] version = "1.0.2" optional = true [dependencies.serde] version = "1.0.160" optional = true [dependencies.serde_derive] version = "1.0.160" optional = true [dependencies.serde_json] version = "1.0.95" optional = true [dependencies.serde_yaml] version = "0.9.21" optional = true [dependencies.thiserror] version = "1.0.58" [dependencies.tokio] version = "1.27.0" features = [ "net", "time", "sync", ] [dependencies.tokio-native-tls] version = "0.3.1" optional = true [dependencies.tokio-rustls] version = "0.24.0" features = ["dangerous_configuration"] optional = true [dependencies.tokio-socks] version = "0.5.1" optional = true [dependencies.tokio-stream] version = "0.1.12" [dependencies.tokio-util] version = "0.7.7" features = ["codec"] [dependencies.toml] version = "0.7.3" optional = true [dependencies.webpki-roots] version = "0.23.0" optional = true [dev-dependencies.anyhow] version = "1.0.81" [dev-dependencies.args] version = "2.2.0" [dev-dependencies.env_logger] version = "0.11.0" [dev-dependencies.futures] version = "0.3.30" [dev-dependencies.getopts] version = "0.2.21" [dev-dependencies.tokio] version = "1.27.0" features = [ "rt", "rt-multi-thread", "macros", "net", "time", ] [features] channel-lists = [] ctcp = [] default = [ "ctcp", "tls-native", "channel-lists", "toml_config", ] json = ["json_config"] json_config = [ "serde", "serde/derive", "serde_derive", "serde_json", ] proxy = ["tokio-socks"] tls-native = [ "native-tls", "tokio-native-tls", ] tls-rust = [ "tokio-rustls", "webpki-roots", "rustls-pemfile", ] toml_config = [ "serde", "serde/derive", "serde_derive", "toml", ] yaml = ["yaml_config"] yaml_config = [ "serde", "serde/derive", "serde_derive", "serde_yaml", ] [badges.is-it-maintained-issue-resolution] repository = "aatxe/irc" [badges.is-it-maintained-open-issues] repository = "aatxe/irc" [badges.travis-ci] repository = "aatxe/irc" irc-1.0.0/Cargo.toml.orig0000644000000053360000000000100105530ustar [package] name = "irc" version = "1.0.0" authors = ["Aaron Weiss "] edition = "2018" rust-version = "1.67" description = "the irc crate – usable, async IRC for Rust " documentation = "https://docs.rs/irc/" readme = "README.md" repository = "https://github.com/aatxe/irc" license = "MPL-2.0" keywords = ["irc", "client", "thread-safe", "async", "tokio"] categories = ["asynchronous", "network-programming"] [badges] travis-ci = { repository = "aatxe/irc" } is-it-maintained-issue-resolution = { repository = "aatxe/irc" } is-it-maintained-open-issues = { repository = "aatxe/irc" } [workspace] members = [ "./", "irc-proto/" ] [features] default = ["ctcp", "tls-native", "channel-lists", "toml_config"] ctcp = [] channel-lists = [] json_config = ["serde", "serde/derive", "serde_derive", "serde_json"] toml_config = ["serde", "serde/derive", "serde_derive", "toml"] yaml_config = ["serde", "serde/derive", "serde_derive", "serde_yaml"] # Temporary transitionary features json = ["json_config"] yaml = ["yaml_config"] proxy = ["tokio-socks"] tls-native = ["native-tls", "tokio-native-tls"] tls-rust = ["tokio-rustls", "webpki-roots", "rustls-pemfile"] [dependencies] chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } encoding = "0.2.33" futures-util = { version = "0.3.30", default-features = false, features = ["alloc", "sink"] } irc-proto = { version = "1.0.0", path = "irc-proto" } log = "0.4.21" parking_lot = "0.12.1" thiserror = "1.0.58" pin-project = "1.0.12" tokio = { version = "1.27.0", features = ["net", "time", "sync"] } tokio-stream = "0.1.12" tokio-util = { version = "0.7.7", features = ["codec"] } # Feature - Config serde = { version = "1.0.160", optional = true } serde_derive = { version = "1.0.160", optional = true } serde_json = { version = "1.0.95", optional = true } serde_yaml = { version = "0.9.21", optional = true } toml = { version = "0.7.3", optional = true } # Feature - Proxy tokio-socks = { version = "0.5.1", optional = true } # Feature - TLS native-tls = { version = "0.2.11", optional = true } tokio-rustls = { version = "0.24.0", features = ["dangerous_configuration"], optional = true } rustls-pemfile = { version = "1.0.2", optional = true } tokio-native-tls = { version = "0.3.1", optional = true } webpki-roots = { version = "0.23.0", optional = true } [dev-dependencies] anyhow = "1.0.81" args = "2.2.0" env_logger = "0.11.0" futures = "0.3.30" getopts = "0.2.21" tokio = { version = "1.27.0", features = ["rt", "rt-multi-thread", "macros", "net", "time"] } [[example]] name = "simple_proxy" path = "examples/simple_proxy.rs" required-features = ["proxy"] [[example]] name = "simple_plaintext" path = "examples/simple_plaintext.rs" required-features = ["tls-native"] irc-1.0.0/Cargo.toml.orig000064400000000000000000000053361046102023000132750ustar 00000000000000[package] name = "irc" version = "1.0.0" authors = ["Aaron Weiss "] edition = "2018" rust-version = "1.67" description = "the irc crate – usable, async IRC for Rust " documentation = "https://docs.rs/irc/" readme = "README.md" repository = "https://github.com/aatxe/irc" license = "MPL-2.0" keywords = ["irc", "client", "thread-safe", "async", "tokio"] categories = ["asynchronous", "network-programming"] [badges] travis-ci = { repository = "aatxe/irc" } is-it-maintained-issue-resolution = { repository = "aatxe/irc" } is-it-maintained-open-issues = { repository = "aatxe/irc" } [workspace] members = [ "./", "irc-proto/" ] [features] default = ["ctcp", "tls-native", "channel-lists", "toml_config"] ctcp = [] channel-lists = [] json_config = ["serde", "serde/derive", "serde_derive", "serde_json"] toml_config = ["serde", "serde/derive", "serde_derive", "toml"] yaml_config = ["serde", "serde/derive", "serde_derive", "serde_yaml"] # Temporary transitionary features json = ["json_config"] yaml = ["yaml_config"] proxy = ["tokio-socks"] tls-native = ["native-tls", "tokio-native-tls"] tls-rust = ["tokio-rustls", "webpki-roots", "rustls-pemfile"] [dependencies] chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } encoding = "0.2.33" futures-util = { version = "0.3.30", default-features = false, features = ["alloc", "sink"] } irc-proto = { version = "1.0.0", path = "irc-proto" } log = "0.4.21" parking_lot = "0.12.1" thiserror = "1.0.58" pin-project = "1.0.12" tokio = { version = "1.27.0", features = ["net", "time", "sync"] } tokio-stream = "0.1.12" tokio-util = { version = "0.7.7", features = ["codec"] } # Feature - Config serde = { version = "1.0.160", optional = true } serde_derive = { version = "1.0.160", optional = true } serde_json = { version = "1.0.95", optional = true } serde_yaml = { version = "0.9.21", optional = true } toml = { version = "0.7.3", optional = true } # Feature - Proxy tokio-socks = { version = "0.5.1", optional = true } # Feature - TLS native-tls = { version = "0.2.11", optional = true } tokio-rustls = { version = "0.24.0", features = ["dangerous_configuration"], optional = true } rustls-pemfile = { version = "1.0.2", optional = true } tokio-native-tls = { version = "0.3.1", optional = true } webpki-roots = { version = "0.23.0", optional = true } [dev-dependencies] anyhow = "1.0.81" args = "2.2.0" env_logger = "0.11.0" futures = "0.3.30" getopts = "0.2.21" tokio = { version = "1.27.0", features = ["rt", "rt-multi-thread", "macros", "net", "time"] } [[example]] name = "simple_proxy" path = "examples/simple_proxy.rs" required-features = ["proxy"] [[example]] name = "simple_plaintext" path = "examples/simple_plaintext.rs" required-features = ["tls-native"] irc-1.0.0/LICENSE.md000064400000000000000000000363331046102023000120130ustar 00000000000000Mozilla Public License Version 2.0 ================================== ### 1. Definitions **1.1. “Contributor”** means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. **1.2. “Contributor Version”** means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. **1.3. “Contribution”** means Covered Software of a particular Contributor. **1.4. “Covered Software”** means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. **1.5. “Incompatible With Secondary Licenses”** means * **(a)** that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or * **(b)** that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. **1.6. “Executable Form”** means any form of the work other than Source Code Form. **1.7. “Larger Work”** means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. **1.8. “License”** means this document. **1.9. “Licensable”** means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. **1.10. “Modifications”** means any of the following: * **(a)** any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or * **(b)** any new file in Source Code Form that contains any Covered Software. **1.11. “Patent Claims” of a Contributor** means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. **1.12. “Secondary License”** means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. **1.13. “Source Code Form”** means the form of the work preferred for making modifications. **1.14. “You” (or “Your”)** means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means **(a)** the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or **(b)** ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. ### 2. License Grants and Conditions #### 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: * **(a)** under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and * **(b)** under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. #### 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. #### 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: * **(a)** for any code that a Contributor has removed from Covered Software; or * **(b)** for infringements caused by: **(i)** Your and any other third party's modifications of Covered Software, or **(ii)** the combination of its Contributions with other software (except as part of its Contributor Version); or * **(c)** under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). #### 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). #### 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. #### 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. #### 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. ### 3. Responsibilities #### 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. #### 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: * **(a)** such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and * **(b)** You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. #### 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). #### 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. #### 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. ### 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: **(a)** comply with the terms of this License to the maximum extent possible; and **(b)** describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. ### 5. Termination **5.1.** The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated **(a)** provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and **(b)** on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. **5.2.** If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ### 6. Disclaimer of Warranty > Covered Software is provided under this License on an “as is” > basis, without warranty of any kind, either expressed, implied, or > statutory, including, without limitation, warranties that the > Covered Software is free of defects, merchantable, fit for a > particular purpose or non-infringing. The entire risk as to the > quality and performance of the Covered Software is with You. > Should any Covered Software prove defective in any respect, You > (not any Contributor) assume the cost of any necessary servicing, > repair, or correction. This disclaimer of warranty constitutes an > essential part of this License. No use of any Covered Software is > authorized under this License except under this disclaimer. ### 7. Limitation of Liability > Under no circumstances and under no legal theory, whether tort > (including negligence), contract, or otherwise, shall any > Contributor, or anyone who distributes Covered Software as > permitted above, be liable to You for any direct, indirect, > special, incidental, or consequential damages of any character > including, without limitation, damages for lost profits, loss of > goodwill, work stoppage, computer failure or malfunction, or any > and all other commercial damages or losses, even if such party > shall have been informed of the possibility of such damages. This > limitation of liability shall not apply to liability for death or > personal injury resulting from such party's negligence to the > extent applicable law prohibits such limitation. Some > jurisdictions do not allow the exclusion or limitation of > incidental or consequential damages, so this exclusion and > limitation may not apply to You. ### 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. ### 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. ### 10. Versions of the License #### 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. #### 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. #### 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. ## Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. ## Exhibit B - “Incompatible With Secondary Licenses” Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. irc-1.0.0/README.md000064400000000000000000000141151046102023000116600ustar 00000000000000# the irc crate [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] ![Downloads][dl-badge] [![Docs][doc-badge]][doc] [ci-badge]: https://github.com/aatxe/irc/actions/workflows/ci.yml/badge.svg [ci]: https://github.com/aatxe/irc/actions/workflows/ci.yml [cr-badge]: https://img.shields.io/crates/v/irc.svg [cr]: https://crates.io/crates/irc [dl-badge]: https://img.shields.io/crates/d/irc.svg [doc-badge]: https://docs.rs/irc/badge.svg [doc]: https://docs.rs/irc [rfc2812]: http://datatracker.ietf.org/doc/html/rfc2812 [ircv3.1]: http://ircv3.net/irc/3.1.html [ircv3.2]: http://ircv3.net/irc/3.2.html "the irc crate" is a thread-safe and async-friendly IRC client library written in Rust. It's compliant with [RFC 2812][rfc2812], [IRCv3.1][ircv3.1], [IRCv3.2][ircv3.2], and includes some additional, common features from popular IRCds. You can find up-to-date, ready-to-use documentation online [on docs.rs][doc]. ## Built with the irc crate the irc crate is being used to build new IRC software in Rust. Here are some of our favorite projects: - [alectro][alectro] — a terminal IRC client - [spilo][spilo] — a minimalistic IRC bouncer - [irc-bot.rs][ircbot] — a library for writing IRC bots - [playbot_ng][playbot_ng] — a Rust-evaluating IRC bot in Rust - [bunnybutt-rs][bunnybutt] — an IRC bot for the [Feed The Beast Wiki][ftb-wiki] - [url-bot-rs][url-bot-rs] — a URL-fetching IRC bot [alectro]: https://github.com/aatxe/alectro [spilo]: https://github.com/aatxe/spilo [ircbot]: https://github.com/8573/irc-bot.rs [bunnybutt]: https://github.com/FTB-Gamepedia/bunnybutt-rs [playbot_ng]: https://github.com/panicbit/playbot_ng [ftb-wiki]: https://ftb.gamepedia.com/FTB_Wiki [url-bot-rs]: https://github.com/nuxeh/url-bot-rs Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pulls) to add it! ## Getting Started To start using the irc crate with cargo, you can add `irc = "0.15"` to your dependencies in your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude]. You'll find a number of examples to help you get started in `examples/`, throughout the documentation, and below. [irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html ## Using Futures The release of v0.14 replaced all existing APIs with one based on async/await. ```rust,no_run,edition2018 use irc::client::prelude::*; use futures::prelude::*; #[tokio::main] async fn main() -> Result<(), failure::Error> { // We can also load the Config at runtime via Config::load("path/to/config.toml") let config = Config { nickname: Some("the-irc-crate".to_owned()), server: Some("chat.freenode.net".to_owned()), channels: vec!["#test".to_owned()], ..Config::default() }; let mut client = Client::from_config(config).await?; client.identify()?; let mut stream = client.stream()?; while let Some(message) = stream.next().await.transpose()? { print!("{}", message); } Ok(()) } ``` Example Cargo.toml file: ```rust,no_run,edition2018 [package] name = "example" version = "0.1.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] irc = "0.15.0" tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros", "net", "time"] } futures = "0.3.0" failure = "0.1.8" ``` ## Configuring IRC Clients As seen above, there are two techniques for configuring the irc crate: runtime loading and programmatic configuration. Runtime loading is done via the function `Config::load`, and is likely sufficient for most IRC bots. Programmatic configuration is convenient for writing tests, but can also be useful when defining your own custom configuration format that can be converted to `Config`. The primary configuration format is TOML, but if you are so inclined, you can use JSON and/or YAML via the optional `json_config` and `yaml_config` features respectively. At the minimum, a configuration requires `nickname` and `server` to be defined, and all other fields are optional. You can find detailed explanations of the various fields on [docs.rs][config-fields]. [config-fields]: https://docs.rs/irc/*/irc/client/data/config/struct.Config.html#fields Alternatively, you can look at the example below of a TOML configuration with all the fields: ```toml owners = [] nickname = "user" nick_password = "password" alt_nicks = ["user_", "user__"] username = "user" realname = "Test User" server = "chat.freenode.net" port = 6697 password = "" proxy_type = "None" proxy_server = "127.0.0.1" proxy_port = "1080" proxy_username = "" proxy_password = "" use_tls = true cert_path = "cert.der" client_cert_path = "client.der" client_cert_pass = "password" encoding = "UTF-8" channels = ["#rust", "#haskell", "#fake"] umodes = "+RB-x" user_info = "I'm a test user for the irc crate." version = "irc:git:Rust" source = "https://github.com/aatxe/irc" ping_time = 180 ping_timeout = 20 burst_window_length = 8 max_messages_in_burst = 15 should_ghost = false ghost_sequence = [] [channel_keys] "#fake" = "password" [options] note = "anything you want can be in here!" and = "you can use it to build your own additional configuration options." key = "value" ``` You can convert between different configuration formats with `convertconf` like so: ```shell cargo run --example convertconf -- -i client_config.json -o client_config.toml ``` Note that the formats are automatically determined based on the selected file extensions. This tool should make it easier for users to migrate their old configurations to TOML. ## Contributing the irc crate is a free, open source library that relies on contributions from its maintainers, Aaron Weiss ([@aatxe][awe]) and Peter Atashian ([@retep998][bun]), as well as the broader Rust community. It's licensed under the Mozilla Public License 2.0 whose text can be found in `LICENSE.md`. To foster an inclusive community around the irc crate, we have adopted a Code of Conduct whose text can be found in `CODE_OF_CONDUCT.md`. You can find details about how to contribute in `CONTRIBUTING.md`. [awe]: https://github.com/aatxe/ [bun]: https://github.com/retep998/ irc-1.0.0/src/client/conn.rs000064400000000000000000000361771046102023000137650ustar 00000000000000//! A module providing IRC connections for use by `IrcServer`s. use futures_util::{sink::Sink, stream::Stream}; use pin_project::pin_project; use std::{ fmt, pin::Pin, task::{Context, Poll}, }; use tokio::net::TcpStream; use tokio::sync::mpsc::UnboundedSender; use tokio_util::codec::Framed; #[cfg(feature = "proxy")] use tokio_socks::tcp::Socks5Stream; #[cfg(feature = "proxy")] use crate::client::data::ProxyType; #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] use std::{fs::File, io::Read}; #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] use native_tls::{Certificate, Identity, TlsConnector}; #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] use tokio_native_tls::{self, TlsStream}; #[cfg(feature = "tls-rust")] use rustls_pemfile::certs; #[cfg(feature = "tls-rust")] use std::{ convert::TryFrom, fs::File, io::{BufReader, Error, ErrorKind}, sync::Arc, }; #[cfg(feature = "tls-rust")] use tokio_rustls::client::TlsStream; #[cfg(feature = "tls-rust")] use tokio_rustls::{ rustls::client::{ServerCertVerified, ServerCertVerifier}, rustls::{ self, Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerName, }, TlsConnector, }; use crate::{ client::{ data::Config, mock::MockStream, transport::{LogView, Logged, Transport}, }, error, proto::{IrcCodec, Message}, }; /// An IRC connection used internally by `IrcServer`. #[pin_project(project = ConnectionProj)] pub enum Connection { #[doc(hidden)] Unsecured(#[pin] Transport), #[doc(hidden)] #[cfg(any(feature = "tls-native", feature = "tls-rust"))] Secured(#[pin] Transport>), #[doc(hidden)] Mock(#[pin] Logged), } impl fmt::Debug for Connection { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match *self { Connection::Unsecured(_) => "Connection::Unsecured(...)", #[cfg(any(feature = "tls-native", feature = "tls-rust"))] Connection::Secured(_) => "Connection::Secured(...)", Connection::Mock(_) => "Connection::Mock(...)", } ) } } impl Connection { /// Creates a new `Connection` using the specified `Config` pub(crate) async fn new( config: &Config, tx: UnboundedSender, ) -> error::Result { if config.use_mock_connection() { log::info!("Connecting via mock to {}.", config.server()?); return Ok(Connection::Mock(Logged::wrap( Self::new_mocked_transport(config, tx).await?, ))); } #[cfg(any(feature = "tls-native", feature = "tls-rust"))] { if config.use_tls() { log::info!("Connecting via TLS to {}.", config.server()?); return Ok(Connection::Secured( Self::new_secured_transport(config, tx).await?, )); } } log::info!("Connecting to {}.", config.server()?); Ok(Connection::Unsecured( Self::new_unsecured_transport(config, tx).await?, )) } #[cfg(not(feature = "proxy"))] async fn new_stream(config: &Config) -> error::Result { Ok(TcpStream::connect((config.server()?, config.port())).await?) } #[cfg(feature = "proxy")] async fn new_stream(config: &Config) -> error::Result { let server = config.server()?; let port = config.port(); let address = (server, port); match config.proxy_type() { ProxyType::None => Ok(TcpStream::connect(address).await?), ProxyType::Socks5 => { let proxy_server = config.proxy_server(); let proxy_port = config.proxy_port(); let proxy = (proxy_server, proxy_port); log::info!("Setup proxy {:?}.", proxy); let proxy_username = config.proxy_username(); let proxy_password = config.proxy_password(); if !proxy_username.is_empty() || !proxy_password.is_empty() { return Ok(Socks5Stream::connect_with_password( proxy, address, proxy_username, proxy_password, ) .await? .into_inner()); } Ok(Socks5Stream::connect(proxy, address).await?.into_inner()) } } } async fn new_unsecured_transport( config: &Config, tx: UnboundedSender, ) -> error::Result> { let stream = Self::new_stream(config).await?; let framed = Framed::new(stream, IrcCodec::new(config.encoding())?); Ok(Transport::new(config, framed, tx)) } #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] async fn new_secured_transport( config: &Config, tx: UnboundedSender, ) -> error::Result>> { let mut builder = TlsConnector::builder(); if let Some(cert_path) = config.cert_path() { if let Ok(mut file) = File::open(cert_path) { let mut cert_data = vec![]; file.read_to_end(&mut cert_data)?; let cert = Certificate::from_der(&cert_data)?; builder.add_root_certificate(cert); log::info!("Added {} to trusted certificates.", cert_path); } else { return Err(error::Error::InvalidConfig { path: config.path(), cause: error::ConfigError::FileMissing { file: cert_path.to_string(), }, }); } } if let Some(client_cert_path) = config.client_cert_path() { if let Ok(mut file) = File::open(client_cert_path) { let mut client_cert_data = vec![]; file.read_to_end(&mut client_cert_data)?; let client_cert_pass = config.client_cert_pass(); let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, client_cert_pass)?; builder.identity(pkcs12_archive); log::info!( "Using {} for client certificate authentication.", client_cert_path ); } else { return Err(error::Error::InvalidConfig { path: config.path(), cause: error::ConfigError::FileMissing { file: client_cert_path.to_string(), }, }); } } if config.dangerously_accept_invalid_certs() { builder.danger_accept_invalid_certs(true); } let connector: tokio_native_tls::TlsConnector = builder.build()?.into(); let domain = config.server()?; let stream = Self::new_stream(config).await?; let stream = connector.connect(domain, stream).await?; let framed = Framed::new(stream, IrcCodec::new(config.encoding())?); Ok(Transport::new(config, framed, tx)) } #[cfg(feature = "tls-rust")] async fn new_secured_transport( config: &Config, tx: UnboundedSender, ) -> error::Result>> { struct DangerousAcceptAllVerifier; impl ServerCertVerifier for DangerousAcceptAllVerifier { fn verify_server_cert( &self, _: &Certificate, _: &[Certificate], _: &ServerName, _: &mut dyn Iterator, _: &[u8], _: std::time::SystemTime, ) -> Result { return Ok(ServerCertVerified::assertion()); } } enum ClientAuth { SingleCert(Vec, PrivateKey), NoClientAuth, } let client_auth = if let Some(client_cert_path) = config.client_cert_path() { if let Ok(file) = File::open(client_cert_path) { let client_cert_data = certs(&mut BufReader::new(file)).map_err(|_| { error::Error::Io(Error::new(ErrorKind::InvalidInput, "invalid cert")) })?; let client_cert_data = client_cert_data .into_iter() .map(Certificate) .collect::>(); let client_cert_pass = PrivateKey(Vec::from(config.client_cert_pass())); log::info!( "Using {} for client certificate authentication.", client_cert_path ); ClientAuth::SingleCert(client_cert_data, client_cert_pass) } else { return Err(error::Error::InvalidConfig { path: config.path(), cause: error::ConfigError::FileMissing { file: client_cert_path.to_string(), }, }); } } else { ClientAuth::NoClientAuth }; macro_rules! make_client_auth { ($builder:expr) => { match client_auth { ClientAuth::SingleCert(data, pass) => { $builder.with_single_cert(data, pass).map_err(|err| { error::Error::Io(Error::new(ErrorKind::InvalidInput, err)) })? } ClientAuth::NoClientAuth => $builder.with_no_client_auth(), } }; } let builder = ClientConfig::builder() .with_safe_default_cipher_suites() .with_safe_default_kx_groups() .with_safe_default_protocol_versions()?; let tls_config = if config.dangerously_accept_invalid_certs() { let builder = builder.with_custom_certificate_verifier(Arc::new(DangerousAcceptAllVerifier)); make_client_auth!(builder) } else { let mut root_store = RootCertStore::empty(); root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map( |ta| { OwnedTrustAnchor::from_subject_spki_name_constraints( ta.subject, ta.spki, ta.name_constraints, ) }, )); if let Some(cert_path) = config.cert_path() { if let Ok(data) = std::fs::read(cert_path) { root_store.add(&Certificate(data)).map_err(|_| { error::Error::Io(Error::new(ErrorKind::InvalidInput, "invalid cert")) })?; log::info!("Added {} to trusted certificates.", cert_path); } else { return Err(error::Error::InvalidConfig { path: config.path(), cause: error::ConfigError::FileMissing { file: cert_path.to_string(), }, }); } } let builder = builder.with_root_certificates(root_store); make_client_auth!(builder) }; let connector = TlsConnector::from(Arc::new(tls_config)); let domain = ServerName::try_from(config.server()?)?; let stream = Self::new_stream(config).await?; let stream = connector.connect(domain, stream).await?; let framed = Framed::new(stream, IrcCodec::new(config.encoding())?); Ok(Transport::new(config, framed, tx)) } async fn new_mocked_transport( config: &Config, tx: UnboundedSender, ) -> error::Result> { use encoding::{label::encoding_from_whatwg_label, EncoderTrap}; let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| { error::Error::UnknownCodec { codec: config.encoding().to_owned(), } })?; let init_str = config.mock_initial_value(); let initial = encoding .encode(init_str, EncoderTrap::Replace) .map_err(|data| error::Error::CodecFailed { codec: encoding.name(), data: data.into_owned(), })?; let stream = MockStream::new(&initial); let framed = Framed::new(stream, IrcCodec::new(config.encoding())?); Ok(Transport::new(config, framed, tx)) } /// Gets a view of the internal logging if and only if this connection is using a mock stream. /// Otherwise, this will always return `None`. This is used for unit testing. pub fn log_view(&self) -> Option { match *self { Connection::Mock(ref inner) => Some(inner.view()), _ => None, } } } impl Stream for Connection { type Item = error::Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.project() { ConnectionProj::Unsecured(inner) => inner.poll_next(cx), #[cfg(any(feature = "tls-native", feature = "tls-rust"))] ConnectionProj::Secured(inner) => inner.poll_next(cx), ConnectionProj::Mock(inner) => inner.poll_next(cx), } } } impl Sink for Connection { type Error = error::Error; fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.project() { ConnectionProj::Unsecured(inner) => inner.poll_ready(cx), #[cfg(any(feature = "tls-native", feature = "tls-rust"))] ConnectionProj::Secured(inner) => inner.poll_ready(cx), ConnectionProj::Mock(inner) => inner.poll_ready(cx), } } fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { match self.project() { ConnectionProj::Unsecured(inner) => inner.start_send(item), #[cfg(any(feature = "tls-native", feature = "tls-rust"))] ConnectionProj::Secured(inner) => inner.start_send(item), ConnectionProj::Mock(inner) => inner.start_send(item), } } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.project() { ConnectionProj::Unsecured(inner) => inner.poll_flush(cx), #[cfg(any(feature = "tls-native", feature = "tls-rust"))] ConnectionProj::Secured(inner) => inner.poll_flush(cx), ConnectionProj::Mock(inner) => inner.poll_flush(cx), } } fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.project() { ConnectionProj::Unsecured(inner) => inner.poll_close(cx), #[cfg(any(feature = "tls-native", feature = "tls-rust"))] ConnectionProj::Secured(inner) => inner.poll_close(cx), ConnectionProj::Mock(inner) => inner.poll_close(cx), } } } irc-1.0.0/src/client/data/client_config.json000064400000000000000000000004121046102023000170500ustar 00000000000000{ "owners": [ "test" ], "nickname": "test", "username": "test", "realname": "test", "password": "", "server": "irc.test.net", "port": 6667, "encoding": "UTF-8", "channels": [ "#test", "#test2" ], "umodes": "+BR", "options": {} }irc-1.0.0/src/client/data/client_config.toml000064400000000000000000000003051046102023000170530ustar 00000000000000owners = ["test"] nickname = "test" username = "test" realname = "test" server = "irc.test.net" port = 6667 password = "" encoding = "UTF-8" channels = ["#test", "#test2"] umodes = "+BR" [options]irc-1.0.0/src/client/data/client_config.yaml000064400000000000000000000002711046102023000170440ustar 00000000000000--- owners: - test nickname: test username: test realname: test server: irc.test.net port: 6667 password: "" encoding: UTF-8 channels: - "#test" - "#test2" umodes: +BR options: {}irc-1.0.0/src/client/data/config.rs000064400000000000000000000722771046102023000152070ustar 00000000000000//! JSON configuration files using serde #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fs::File, io::prelude::*, path::{Path, PathBuf}, }; #[cfg(feature = "json_config")] use serde_json; #[cfg(feature = "yaml_config")] use serde_yaml; #[cfg(feature = "toml_config")] use toml; #[cfg(feature = "proxy")] use crate::client::data::proxy::ProxyType; use crate::error::Error::InvalidConfig; #[cfg(feature = "toml_config")] use crate::error::TomlError; use crate::error::{ConfigError, Result}; /// Configuration for IRC clients. /// /// # Building a configuration programmatically /// /// For some use cases, it may be useful to build configurations programmatically. Since `Config` is /// an ordinary struct with public fields, this should be rather straightforward. However, it is /// important to note that the use of `Config::default()` is important, even when specifying all /// visible fields because `Config` keeps track of whether it was loaded from a file or /// programmatically defined, in order to produce better error messages. Using `Config::default()` /// as below will ensure that this process is handled correctly. /// /// ``` /// # extern crate irc; /// use irc::client::prelude::Config; /// /// # fn main() { /// let config = Config { /// nickname: Some("test".to_owned()), /// server: Some("irc.example.com".to_owned()), /// ..Config::default() /// }; /// # } /// ``` /// /// # Loading a configuration from a file /// /// The standard method of using a configuration is to load it from a TOML file. You can find an /// example TOML configuration in the README, as well as a minimal example with code for loading the /// configuration below. /// /// ## TOML (`config.toml`) /// ```toml /// nickname = "test" /// server = "irc.example.com" /// ``` /// /// ## Rust /// ```no_run /// # extern crate irc; /// use irc::client::prelude::Config; /// /// # fn main() { /// let config = Config::load("config.toml").unwrap(); /// # } /// ``` #[derive(Clone, Default, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Config { /// A list of the owners of the client by nickname (for bots). #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub owners: Vec, /// The client's nickname. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub nickname: Option, /// The client's NICKSERV password. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub nick_password: Option, /// Alternative nicknames for the client, if the default is taken. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub alt_nicks: Vec, /// The client's username. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub username: Option, /// The client's real name. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub realname: Option, /// The server to connect to. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub server: Option, /// The port to connect on. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub port: Option, /// The password to connect to the server. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub password: Option, /// The proxy type to connect to. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_type: Option, /// The proxy server to connect to. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_server: Option, /// The proxy port to connect on. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_port: Option, /// The username to connect to the proxy server. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_username: Option, /// The password to connect to the proxy server. #[cfg(feature = "proxy")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub proxy_password: Option, /// Whether or not to use TLS. /// Clients will automatically panic if this is enabled without TLS support. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub use_tls: Option, /// The path to the TLS certificate for this server in DER format. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub cert_path: Option, /// The path to a TLS certificate to use for CertFP client authentication in a DER-formatted /// PKCS #12 archive. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub client_cert_path: Option, /// The password for the certificate to use in CertFP authentication. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub client_cert_pass: Option, /// On `true`, all certificate validations are skipped. Defaults to `false`. /// /// # Warning /// You should think very carefully before using this method. If invalid hostnames are trusted, *any* valid /// certificate for *any* site will be trusted for use. This introduces significant vulnerabilities, and should /// only be used as a last resort. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub dangerously_accept_invalid_certs: Option, /// The encoding type used for this connection. /// This is typically UTF-8, but could be something else. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub encoding: Option, /// A list of channels to join on connection. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub channels: Vec, /// User modes to set on connect. Example: "+RB -x" #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub umodes: Option, /// The text that'll be sent in response to CTCP USERINFO requests. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub user_info: Option, /// The text that'll be sent in response to CTCP VERSION requests. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub version: Option, /// The text that'll be sent in response to CTCP SOURCE requests. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub source: Option, /// The amount of inactivity in seconds before the client will ping the server. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub ping_time: Option, /// The amount of time in seconds for a client to reconnect due to no ping response. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub ping_timeout: Option, /// The length in seconds of a rolling window for message throttling. If more than /// `max_messages_in_burst` messages are sent within `burst_window_length` seconds, additional /// messages will be delayed automatically as appropriate. In particular, in the past /// `burst_window_length` seconds, there will never be more than `max_messages_in_burst` messages /// sent. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub burst_window_length: Option, /// The maximum number of messages that can be sent in a burst window before they'll be delayed. /// Messages are automatically delayed as appropriate. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub max_messages_in_burst: Option, /// Whether the client should use NickServ GHOST to reclaim its primary nickname if it is in /// use. This has no effect if `nick_password` is not set. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] #[cfg_attr(feature = "serde", serde(default))] pub should_ghost: bool, /// The command(s) that should be sent to NickServ to recover a nickname. The nickname and /// password will be appended in that order after the command. /// E.g. `["RECOVER", "RELEASE"]` means `RECOVER nick pass` and `RELEASE nick pass` will be sent /// in that order. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(default))] pub ghost_sequence: Option>, /// Whether or not to use a fake connection for testing purposes. You probably will never want /// to enable this, but it is used in unit testing for the `irc` crate. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] #[cfg_attr(feature = "serde", serde(default))] pub use_mock_connection: bool, /// The initial value used by the fake connection for testing. You probably will never need to /// set this, but it is used in unit testing for the `irc` crate. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub mock_initial_value: Option, /// A mapping of channel names to keys for join-on-connect. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub channel_keys: HashMap, /// A map of additional options to be stored in config. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))] #[cfg_attr(feature = "serde", serde(default))] pub options: HashMap, /// The path that this configuration was loaded from. /// /// This should not be specified in any configuration. It will automatically be handled by the library. #[cfg_attr(feature = "serde", serde(skip_serializing))] #[doc(hidden)] pub path: Option, } #[cfg(feature = "serde")] fn is_false(v: &bool) -> bool { !v } impl Config { fn with_path>(mut self, path: P) -> Config { self.path = Some(path.as_ref().to_owned()); self } /// Returns the location this Config was loaded from or ``. pub(crate) fn path(&self) -> String { self.path .as_ref() .map(|buf| buf.to_string_lossy().into_owned()) .unwrap_or_else(|| "".to_owned()) } /// Loads a configuration from the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only toml is available by default. pub fn load>(path: P) -> Result { let mut file = File::open(&path)?; let mut data = String::new(); file.read_to_string(&mut data)?; let res = match path.as_ref().extension().and_then(|s| s.to_str()) { Some("json") => Config::load_json(&path, &data), Some("toml") => Config::load_toml(&path, &data), Some("yaml") | Some("yml") => Config::load_yaml(&path, &data), Some(ext) => Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::UnknownConfigFormat { format: ext.to_owned(), }, }), None => Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::MissingExtension, }), }; res.map(|config| config.with_path(path)) } #[cfg(feature = "json_config")] fn load_json>(path: P, data: &str) -> Result { serde_json::from_str(data).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidJson(e), }) } #[cfg(not(feature = "json_config"))] fn load_json>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "JSON" }, }) } #[cfg(feature = "toml_config")] fn load_toml>(path: P, data: &str) -> Result { toml::from_str(data).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidToml(TomlError::Read(e)), }) } #[cfg(not(feature = "toml_config"))] fn load_toml>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "TOML" }, }) } #[cfg(feature = "yaml_config")] fn load_yaml>(path: P, data: &str) -> Result { serde_yaml::from_str(data).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidYaml(e), }) } #[cfg(not(feature = "yaml_config"))] fn load_yaml>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "YAML" }, }) } /// Saves a configuration to the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only toml is available by default. pub fn save>(&mut self, path: P) -> Result<()> { let _ = self.path.take(); let mut file = File::create(&path)?; let data = match path.as_ref().extension().and_then(|s| s.to_str()) { Some("json") => self.save_json(&path)?, Some("toml") => self.save_toml(&path)?, Some("yaml") | Some("yml") => self.save_yaml(&path)?, Some(ext) => { return Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::UnknownConfigFormat { format: ext.to_owned(), }, }) } None => { return Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::MissingExtension, }) } }; file.write_all(data.as_bytes())?; self.path = Some(path.as_ref().to_owned()); Ok(()) } #[cfg(feature = "json_config")] fn save_json>(&self, path: &P) -> Result { serde_json::to_string(self).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidJson(e), }) } #[cfg(not(feature = "json_config"))] fn save_json>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "JSON" }, }) } #[cfg(feature = "toml_config")] fn save_toml>(&self, path: &P) -> Result { toml::to_string(self).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidToml(TomlError::Write(e)), }) } #[cfg(not(feature = "toml_config"))] fn save_toml>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "TOML" }, }) } #[cfg(feature = "yaml_config")] fn save_yaml>(&self, path: &P) -> Result { serde_yaml::to_string(self).map_err(|e| InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidYaml(e), }) } #[cfg(not(feature = "yaml_config"))] fn save_yaml>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::ConfigFormatDisabled { format: "YAML" }, }) } /// Determines whether or not the nickname provided is the owner of the bot. pub fn is_owner(&self, nickname: &str) -> bool { self.owners.iter().any(|n| n == nickname) } /// Gets the nickname specified in the configuration. pub fn nickname(&self) -> Result<&str> { self.nickname.as_deref().ok_or_else(|| InvalidConfig { path: self.path(), cause: ConfigError::NicknameNotSpecified, }) } /// Gets the bot's nickserv password specified in the configuration. /// This defaults to an empty string when not specified. pub fn nick_password(&self) -> &str { self.nick_password.as_ref().map_or("", String::as_str) } /// Gets the alternate nicknames specified in the configuration. /// This defaults to an empty vector when not specified. pub fn alternate_nicknames(&self) -> &[String] { &self.alt_nicks } /// Gets the username specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn username(&self) -> &str { self.username .as_ref() .map_or(self.nickname().unwrap_or("user"), |s| s) } /// Gets the real name specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn real_name(&self) -> &str { self.realname .as_ref() .map_or(self.nickname().unwrap_or("irc"), |s| s) } /// Gets the address of the server specified in the configuration. pub fn server(&self) -> Result<&str> { self.server.as_deref().ok_or_else(|| InvalidConfig { path: self.path(), cause: ConfigError::ServerNotSpecified, }) } /// Gets the port of the server specified in the configuration. /// This defaults to 6697 (or 6667 if use_tls is specified as false) when not specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn port(&self) -> u16 { self.port.as_ref().cloned().unwrap_or(match self.use_tls() { true => 6697, false => 6667, }) } /// Gets the port of the server specified in the configuration. /// This defaults to 6667 when not specified. #[cfg(not(any(feature = "tls-native", feature = "tls-rust")))] pub fn port(&self) -> u16 { self.port.as_ref().cloned().unwrap_or(6667) } /// Gets the server password specified in the configuration. /// This defaults to an empty string when not specified. pub fn password(&self) -> &str { self.password.as_ref().map_or("", String::as_str) } /// Gets the type of the proxy specified in the configuration. /// This defaults to a None ProxyType when not specified. #[cfg(feature = "proxy")] pub fn proxy_type(&self) -> ProxyType { self.proxy_type.as_ref().cloned().unwrap_or(ProxyType::None) } /// Gets the address of the proxy specified in the configuration. /// This defaults to "localhost" string when not specified. #[cfg(feature = "proxy")] pub fn proxy_server(&self) -> &str { self.proxy_server .as_ref() .map_or("localhost", String::as_str) } /// Gets the port of the proxy specified in the configuration. /// This defaults to 1080 when not specified. #[cfg(feature = "proxy")] pub fn proxy_port(&self) -> u16 { self.proxy_port.as_ref().cloned().unwrap_or(1080) } /// Gets the username of the proxy specified in the configuration. /// This defaults to an empty string when not specified. #[cfg(feature = "proxy")] pub fn proxy_username(&self) -> &str { self.proxy_username.as_ref().map_or("", String::as_str) } /// Gets the password of the proxy specified in the configuration. /// This defaults to an empty string when not specified. #[cfg(feature = "proxy")] pub fn proxy_password(&self) -> &str { self.proxy_password.as_ref().map_or("", String::as_str) } /// Gets whether or not to use TLS with this connection. /// This defaults to true when not specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn use_tls(&self) -> bool { self.use_tls.as_ref().cloned().map_or(true, |s| s) } /// Gets the path to the TLS certificate in DER format if specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn cert_path(&self) -> Option<&str> { self.cert_path.as_deref() } /// Gets whether or not to dangerously accept invalid certificates. /// This defaults to `false` when not specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn dangerously_accept_invalid_certs(&self) -> bool { self.dangerously_accept_invalid_certs .as_ref() .cloned() .unwrap_or(false) } /// Gets the path to the client authentication certificate in DER format if specified. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn client_cert_path(&self) -> Option<&str> { self.client_cert_path.as_deref() } /// Gets the password to the client authentication certificate. #[cfg(any(feature = "tls-native", feature = "tls-rust"))] pub fn client_cert_pass(&self) -> &str { self.client_cert_pass.as_ref().map_or("", String::as_str) } /// Gets the encoding to use for this connection. This requires the encode feature to work. /// This defaults to UTF-8 when not specified. pub fn encoding(&self) -> &str { self.encoding.as_ref().map_or("UTF-8", |s| s) } /// Gets the channels to join upon connection. /// This defaults to an empty vector if it's not specified. pub fn channels(&self) -> &[String] { &self.channels } /// Gets the key for the specified channel if it exists in the configuration. pub fn channel_key(&self, chan: &str) -> Option<&str> { self.channel_keys.get(chan).map(String::as_str) } /// Gets the user modes to set on connect specified in the configuration. /// This defaults to an empty string when not specified. pub fn umodes(&self) -> &str { self.umodes.as_ref().map_or("", String::as_str) } /// Gets the string to be sent in response to CTCP USERINFO requests. /// This defaults to an empty string when not specified. pub fn user_info(&self) -> &str { self.user_info.as_ref().map_or("", String::as_str) } /// Gets the string to be sent in response to CTCP VERSION requests. /// This defaults to `irc:version:env` when not specified. /// For example, `irc:0.12.0:Compiled with rustc` pub fn version(&self) -> &str { self.version.as_ref().map_or(crate::VERSION_STR, |s| s) } /// Gets the string to be sent in response to CTCP SOURCE requests. /// This defaults to `https://github.com/aatxe/irc` when not specified. pub fn source(&self) -> &str { self.source .as_ref() .map_or("https://github.com/aatxe/irc", String::as_str) } /// Gets the amount of time in seconds for the interval at which the client pings the server. /// This defaults to 180 seconds when not specified. pub fn ping_time(&self) -> u32 { self.ping_time.as_ref().cloned().unwrap_or(180) } /// Gets the amount of time in seconds for the client to disconnect after not receiving a ping /// response. /// This defaults to 20 seconds when not specified. pub fn ping_timeout(&self) -> u32 { self.ping_timeout.as_ref().cloned().unwrap_or(20) } /// The amount of time in seconds to consider a window for burst messages. The message throttling /// system maintains the invariant that in the past `burst_window_length` seconds, the maximum /// number of messages sent is `max_messages_in_burst`. /// This defaults to 8 seconds when not specified. pub fn burst_window_length(&self) -> u32 { self.burst_window_length.as_ref().cloned().unwrap_or(8) } /// The maximum number of messages that can be sent in a burst window before they'll be delayed. /// Messages are automatically delayed until the start of the next window. The message throttling /// system maintains the invariant that in the past `burst_window_length` seconds, the maximum /// number of messages sent is `max_messages_in_burst`. /// This defaults to 15 messages when not specified. pub fn max_messages_in_burst(&self) -> u32 { self.max_messages_in_burst.as_ref().cloned().unwrap_or(15) } /// Gets whether or not to attempt nickname reclamation using NickServ GHOST. /// This defaults to false when not specified. pub fn should_ghost(&self) -> bool { self.should_ghost } /// Gets the NickServ command sequence to recover a nickname. /// This defaults to `["GHOST"]` when not specified. pub fn ghost_sequence(&self) -> Option<&[String]> { self.ghost_sequence.as_deref() } /// Looks up the specified string in the options map. pub fn get_option(&self, option: &str) -> Option<&str> { self.options.get(option).map(String::as_str) } /// Gets whether or not to use a mock connection for testing. /// This defaults to false when not specified. pub fn use_mock_connection(&self) -> bool { self.use_mock_connection } /// Gets the initial value for the mock connection. /// This defaults to false when not specified. /// This has no effect if `use_mock_connection` is not `true`. pub fn mock_initial_value(&self) -> &str { self.mock_initial_value.as_ref().map_or("", |s| s) } } #[cfg(test)] mod test { use super::Config; use std::collections::HashMap; #[cfg(any( feature = "json_config", feature = "toml_config", feature = "yaml_config" ))] use super::Result; #[allow(unused)] fn test_config() -> Config { Config { owners: vec!["test".to_string()], nickname: Some("test".to_string()), username: Some("test".to_string()), realname: Some("test".to_string()), password: Some(String::new()), umodes: Some("+BR".to_string()), server: Some("irc.test.net".to_string()), port: Some(6667), encoding: Some("UTF-8".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], ..Default::default() } } #[test] fn is_owner() { let cfg = Config { owners: vec!["test".to_string(), "test2".to_string()], ..Default::default() }; assert!(cfg.is_owner("test")); assert!(cfg.is_owner("test2")); assert!(!cfg.is_owner("test3")); } #[test] fn get_option() { let cfg = Config { options: { let mut map = HashMap::new(); map.insert("testing".to_string(), "test".to_string()); map }, ..Default::default() }; assert_eq!(cfg.get_option("testing"), Some("test")); assert_eq!(cfg.get_option("not"), None); } #[test] #[cfg(feature = "json_config")] fn load_from_json() -> Result<()> { const DATA: &str = include_str!("client_config.json"); assert_eq!( Config::load_json("client_config.json", DATA)?.with_path("client_config.json"), test_config().with_path("client_config.json") ); Ok(()) } #[test] #[cfg(feature = "toml_config")] fn load_from_toml() -> Result<()> { const DATA: &str = include_str!("client_config.toml"); assert_eq!( Config::load_toml("client_config.toml", DATA)?.with_path("client_config.toml"), test_config().with_path("client_config.toml") ); Ok(()) } #[test] #[cfg(feature = "yaml_config")] fn load_from_yaml() -> Result<()> { const DATA: &str = include_str!("client_config.yaml"); assert_eq!( Config::load_yaml("client_config.yaml", DATA)?.with_path("client_config.yaml"), test_config().with_path("client_config.yaml") ); Ok(()) } } irc-1.0.0/src/client/data/mod.rs000064400000000000000000000004361046102023000145050ustar 00000000000000//! Data related to IRC functionality. pub use crate::client::data::config::Config; #[cfg(feature = "proxy")] pub use crate::client::data::proxy::ProxyType; pub use crate::client::data::user::{AccessLevel, User}; pub mod config; #[cfg(feature = "proxy")] pub mod proxy; pub mod user; irc-1.0.0/src/client/data/proxy.rs000064400000000000000000000015571046102023000151140ustar 00000000000000//! A feature which allow us to connect to IRC via a proxy. //! //! ``` //! use irc::client::prelude::Config; //! use irc::client::data::ProxyType; //! //! # fn main() { //! let config = Config { //! nickname: Some("test".to_owned()), //! server: Some("irc.example.com".to_owned()), //! proxy_type: Some(ProxyType::Socks5), //! proxy_server: Some("127.0.0.1".to_owned()), //! proxy_port: Some(9050), //! ..Config::default() //! }; //! # } //! ``` #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; /// An enum which defines which type of proxy should be in use. #[cfg(feature = "proxy")] #[derive(Clone, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ProxyType { /// Does not use any proxy. None, /// Use a SOCKS5 proxy. /// DNS queries are also sent via the proxy. Socks5, } irc-1.0.0/src/client/data/user.rs000064400000000000000000000276431046102023000147150ustar 00000000000000//! Data for tracking user information. use std::borrow::ToOwned; use std::cmp::Ordering; use std::cmp::Ordering::{Equal, Greater, Less}; use std::str::FromStr; use crate::proto::{ChannelMode, Mode}; /// IRC User data. #[derive(Clone, Debug)] pub struct User { /// The user's nickname. nickname: String, /// The user's username. username: Option, /// The user's hostname. hostname: Option, /// The user's highest access level. highest_access_level: AccessLevel, /// All of the user's current access levels. access_levels: Vec, } impl User { /// Creates a new User. pub fn new(string: &str) -> User { let ranks: Vec<_> = AccessLevelIterator::new(string).collect(); let mut state = &string[ranks.len()..]; let nickname = state.find('!').map_or(state, |i| &state[..i]).to_owned(); state = state.find('!').map_or("", |i| &state[i + 1..]); let username = state.find('@').map(|i| state[..i].to_owned()); let hostname = state.find('@').map(|i| state[i + 1..].to_owned()); User { nickname, username, hostname, access_levels: { let mut ranks = ranks.clone(); ranks.push(AccessLevel::Member); ranks }, highest_access_level: { let mut max = AccessLevel::Member; for rank in ranks { if rank > max { max = rank } } max }, } } /// Gets the nickname of the user. pub fn get_nickname(&self) -> &str { &self.nickname } /// Gets the username of the user, if it's known. /// This requires the IRCv3.2 extension `userhost-in-name`. pub fn get_username(&self) -> Option<&str> { self.username.as_ref().map(|s| &s[..]) } /// Gets the hostname of the user, if it's known. /// This requires the IRCv3.2 extension `userhost-in-name`. pub fn get_hostname(&self) -> Option<&str> { self.hostname.as_ref().map(|s| &s[..]) } /// Gets the user's highest access level. pub fn highest_access_level(&self) -> AccessLevel { self.highest_access_level } /// Gets all the user's access levels. pub fn access_levels(&self) -> Vec { self.access_levels.clone() } /// Updates the user's access level. pub fn update_access_level(&mut self, mode: &Mode) { match *mode { Mode::Plus(ChannelMode::Founder, _) => self.add_access_level(AccessLevel::Owner), Mode::Minus(ChannelMode::Founder, _) => self.sub_access_level(AccessLevel::Owner), Mode::Plus(ChannelMode::Admin, _) => self.add_access_level(AccessLevel::Admin), Mode::Minus(ChannelMode::Admin, _) => self.sub_access_level(AccessLevel::Admin), Mode::Plus(ChannelMode::Oper, _) => self.add_access_level(AccessLevel::Oper), Mode::Minus(ChannelMode::Oper, _) => self.sub_access_level(AccessLevel::Oper), Mode::Plus(ChannelMode::Halfop, _) => self.add_access_level(AccessLevel::HalfOp), Mode::Minus(ChannelMode::Halfop, _) => self.sub_access_level(AccessLevel::HalfOp), Mode::Plus(ChannelMode::Voice, _) => self.add_access_level(AccessLevel::Voice), Mode::Minus(ChannelMode::Voice, _) => self.sub_access_level(AccessLevel::Voice), _ => {} } } /// Adds an access level to the list, and updates the highest level if necessary. fn add_access_level(&mut self, level: AccessLevel) { if level > self.highest_access_level() { self.highest_access_level = level } self.access_levels.push(level) } /// Removes an access level from the list, and updates the highest level if necessary. fn sub_access_level(&mut self, level: AccessLevel) { if let Some(n) = self.access_levels.iter().position(|x| *x == level) { self.access_levels.swap_remove(n); } if level == self.highest_access_level() { self.highest_access_level = { let mut max = AccessLevel::Member; for level in &self.access_levels { if level > &max { max = *level } } max } } } } impl PartialEq for User { fn eq(&self, other: &User) -> bool { self.nickname == other.nickname && self.username == other.username && self.hostname == other.hostname } } /// The user's access level. #[derive(Copy, PartialEq, Clone, Debug)] pub enum AccessLevel { /// The channel owner (~). Owner, /// A channel administrator (&). Admin, /// A channel operator (@), Oper, /// A channel half-oper (%), HalfOp, /// A user with voice (+), Voice, /// A normal user, Member, } impl PartialOrd for AccessLevel { fn partial_cmp(&self, other: &AccessLevel) -> Option { if self == other { return Some(Equal); } match *self { AccessLevel::Owner => Some(Greater), AccessLevel::Admin => { if other == &AccessLevel::Owner { Some(Less) } else { Some(Greater) } } AccessLevel::Oper => { if other == &AccessLevel::Owner || other == &AccessLevel::Admin { Some(Less) } else { Some(Greater) } } AccessLevel::HalfOp => { if other == &AccessLevel::Voice || other == &AccessLevel::Member { Some(Greater) } else { Some(Less) } } AccessLevel::Voice => { if other == &AccessLevel::Member { Some(Greater) } else { Some(Less) } } AccessLevel::Member => Some(Less), } } } impl FromStr for AccessLevel { type Err = &'static str; fn from_str(s: &str) -> Result { match s.chars().next() { Some('~') => Ok(AccessLevel::Owner), Some('&') => Ok(AccessLevel::Admin), Some('@') => Ok(AccessLevel::Oper), Some('%') => Ok(AccessLevel::HalfOp), Some('+') => Ok(AccessLevel::Voice), None => Err("No access level in an empty string."), _ => Err("Failed to parse access level."), } } } /// An iterator used to parse access levels from strings. struct AccessLevelIterator { value: String, } impl AccessLevelIterator { pub fn new(value: &str) -> AccessLevelIterator { AccessLevelIterator { value: value.to_owned(), } } } impl Iterator for AccessLevelIterator { type Item = AccessLevel; fn next(&mut self) -> Option { let ret = self.value.parse(); if !self.value.is_empty() { self.value = self.value.chars().skip(1).collect() } ret.ok() } } #[cfg(test)] mod test { use super::AccessLevel::*; use super::{AccessLevel, User}; use crate::proto::ChannelMode as M; use crate::proto::Mode::*; #[test] fn parse_access_level() { assert!("member".parse::().is_err()); assert_eq!("~owner".parse::().unwrap(), Owner); assert_eq!("&admin".parse::().unwrap(), Admin); assert_eq!("@oper".parse::().unwrap(), Oper); assert_eq!("%halfop".parse::().unwrap(), HalfOp); assert_eq!("+voice".parse::().unwrap(), Voice); assert!("".parse::().is_err()); } #[test] fn create_user() { let user = User::new("~owner"); let exp = User { nickname: "owner".to_string(), username: None, hostname: None, highest_access_level: Owner, access_levels: vec![Owner, Member], }; assert_eq!(user, exp); assert_eq!(user.highest_access_level, exp.highest_access_level); assert_eq!(user.access_levels, exp.access_levels); } #[test] fn create_user_complex() { let user = User::new("~&+user"); let exp = User { nickname: "user".to_string(), username: None, hostname: None, highest_access_level: Owner, access_levels: vec![Owner, Admin, Voice, Member], }; assert_eq!(user, exp); assert_eq!(user.highest_access_level, exp.highest_access_level); assert_eq!(user.access_levels, exp.access_levels); } #[test] fn get_nickname() { let user = User::new("~owner"); assert_eq!(user.get_nickname(), "owner"); } #[test] fn get_username() { let user = User::new("user!username@hostname"); assert_eq!(user.get_username(), Some("username")); let user = User::new("user"); assert_eq!(user.get_username(), None); } #[test] fn get_hostname() { let user = User::new("user!username@hostname"); assert_eq!(user.get_hostname(), Some("hostname")); let user = User::new("user"); assert_eq!(user.get_hostname(), None); } #[test] fn access_level() { let user = User::new("~owner"); assert_eq!(user.highest_access_level(), Owner); } #[test] fn update_user_rank() { let mut user = User::new("user"); assert_eq!(user.highest_access_level, Member); user.update_access_level(&Plus(M::Founder, None)); assert_eq!(user.highest_access_level, Owner); user.update_access_level(&Minus(M::Founder, None)); assert_eq!(user.highest_access_level, Member); user.update_access_level(&Plus(M::Admin, None)); assert_eq!(user.highest_access_level, Admin); user.update_access_level(&Minus(M::Admin, None)); assert_eq!(user.highest_access_level, Member); user.update_access_level(&Plus(M::Oper, None)); assert_eq!(user.highest_access_level, Oper); user.update_access_level(&Minus(M::Oper, None)); assert_eq!(user.highest_access_level, Member); user.update_access_level(&Plus(M::Halfop, None)); assert_eq!(user.highest_access_level, HalfOp); user.update_access_level(&Minus(M::Halfop, None)); assert_eq!(user.highest_access_level, Member); user.update_access_level(&Plus(M::Voice, None)); assert_eq!(user.highest_access_level, Voice); user.update_access_level(&Minus(M::Voice, None)); assert_eq!(user.highest_access_level, Member); } #[test] fn derank_user_in_full() { let mut user = User::new("~&@%+user"); assert_eq!(user.highest_access_level, Owner); assert_eq!( user.access_levels, vec![Owner, Admin, Oper, HalfOp, Voice, Member] ); user.update_access_level(&Minus(M::Halfop, None)); assert_eq!(user.highest_access_level, Owner); assert_eq!(user.access_levels, vec![Owner, Admin, Oper, Member, Voice]); user.update_access_level(&Minus(M::Founder, None)); assert_eq!(user.highest_access_level, Admin); assert_eq!(user.access_levels, vec![Voice, Admin, Oper, Member]); user.update_access_level(&Minus(M::Admin, None)); assert_eq!(user.highest_access_level, Oper); assert_eq!(user.access_levels, vec![Voice, Member, Oper]); user.update_access_level(&Minus(M::Oper, None)); assert_eq!(user.highest_access_level, Voice); assert_eq!(user.access_levels, vec![Voice, Member]); user.update_access_level(&Minus(M::Voice, None)); assert_eq!(user.highest_access_level, Member); assert_eq!(user.access_levels, vec![Member]); } } irc-1.0.0/src/client/mock.rs000064400000000000000000000035051046102023000137460ustar 00000000000000use std::{ io::{self, Cursor, Read, Write}, pin::Pin, task::{Context, Poll}, }; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; /// A fake stream for testing network applications backed by buffers. #[derive(Clone, Debug)] pub struct MockStream { written: Cursor>, received: Cursor>, } impl MockStream { /// Creates a new mock stream with nothing to read. pub fn empty() -> MockStream { MockStream::new(&[]) } /// Creates a new mock stream with the specified bytes to read. pub fn new(initial: &[u8]) -> MockStream { MockStream { written: Cursor::new(vec![]), received: Cursor::new(initial.to_owned()), } } /// Gets a slice of bytes representing the data that has been written. pub fn written(&self) -> &[u8] { self.written.get_ref() } /// Gets a slice of bytes representing the data that has been received. pub fn received(&self) -> &[u8] { self.received.get_ref() } } impl AsyncRead for MockStream { fn poll_read( mut self: Pin<&mut Self>, _: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let n = self.as_mut().received.read(buf.initialize_unfilled())?; buf.advance(n); Poll::Ready(Ok(())) } } impl AsyncWrite for MockStream { fn poll_write( mut self: Pin<&mut Self>, _: &mut Context<'_>, buf: &[u8], ) -> Poll> { Poll::Ready(self.as_mut().written.write(buf)) } fn poll_flush(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(self.as_mut().written.flush()) } fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } } irc-1.0.0/src/client/mod.rs000064400000000000000000002013541046102023000135760ustar 00000000000000//! A simple, thread-safe, and async-friendly IRC client library. //! //! This API provides the ability to connect to an IRC server via the //! [`Client`] type. The [`Client`] trait that //! [`Client`] implements provides methods for communicating with the //! server. //! //! # Examples //! //! Using these APIs, we can connect to a server and send a one-off message (in this case, //! identifying with the server). //! //! ```no_run //! # extern crate irc; //! use irc::client::prelude::Client; //! //! # #[tokio::main] //! # async fn main() -> irc::error::Result<()> { //! let client = Client::new("config.toml").await?; //! client.identify()?; //! # Ok(()) //! # } //! ``` //! //! We can then use functions from [`Client`] to receive messages from the //! server in a blocking fashion and perform any desired actions in response. The following code //! performs a simple call-and-response when the bot's name is mentioned in a channel. //! //! ```no_run //! use irc::client::prelude::*; //! use futures::*; //! //! # #[tokio::main] //! # async fn main() -> irc::error::Result<()> { //! let mut client = Client::new("config.toml").await?; //! let mut stream = client.stream()?; //! client.identify()?; //! //! while let Some(message) = stream.next().await.transpose()? { //! if let Command::PRIVMSG(channel, message) = message.command { //! if message.contains(client.current_nickname()) { //! client.send_privmsg(&channel, "beep boop").unwrap(); //! } //! } //! } //! # Ok(()) //! # } //! ``` #[cfg(feature = "ctcp")] use chrono::prelude::*; use futures_util::{ future::{FusedFuture, Future}, ready, stream::{FusedStream, Stream}, }; use futures_util::{ sink::Sink as _, stream::{SplitSink, SplitStream, StreamExt as _}, }; use parking_lot::RwLock; use std::{ collections::HashMap, fmt, path::Path, pin::Pin, sync::Arc, task::{Context, Poll}, }; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use crate::{ client::{ conn::Connection, data::{Config, User}, }, error, proto::{ mode::ModeType, CapSubCommand::{END, LS, REQ}, Capability, ChannelMode, Command, Command::{ ChannelMODE, AUTHENTICATE, CAP, INVITE, JOIN, KICK, KILL, NICK, NICKSERV, NOTICE, OPER, PART, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER, }, Message, Mode, NegotiationVersion, Response, }, }; pub mod conn; pub mod data; mod mock; pub mod prelude; pub mod transport; macro_rules! pub_state_base { () => { /// Changes the modes for the specified target. pub fn send_mode(&self, target: S, modes: &[Mode]) -> error::Result<()> where S: fmt::Display, T: ModeType, { self.send(T::mode(&target.to_string(), modes)) } /// Joins the specified channel or chanlist. pub fn send_join(&self, chanlist: S) -> error::Result<()> where S: fmt::Display, { self.send(JOIN(chanlist.to_string(), None, None)) } /// Joins the specified channel or chanlist using the specified key or keylist. pub fn send_join_with_keys(&self, chanlist: S1, keylist: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { self.send(JOIN(chanlist.to_string(), Some(keylist.to_string()), None)) } /// Sends a notice to the specified target. pub fn send_notice(&self, target: S1, message: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { let message = message.to_string(); for line in message.split("\r\n") { self.send(NOTICE(target.to_string(), line.to_string()))? } Ok(()) } }; } macro_rules! pub_sender_base { () => { /// Sends a request for a list of server capabilities for a specific IRCv3 version. pub fn send_cap_ls(&self, version: NegotiationVersion) -> error::Result<()> { self.send(Command::CAP( None, LS, match version { NegotiationVersion::V301 => None, NegotiationVersion::V302 => Some("302".to_owned()), }, None, )) } /// Sends an IRCv3 capabilities request for the specified extensions. pub fn send_cap_req(&self, extensions: &[Capability]) -> error::Result<()> { let append = |mut s: String, c| { s.push_str(c); s.push(' '); s }; let mut exts = extensions .iter() .map(|c| c.as_ref()) .fold(String::new(), append); let len = exts.len() - 1; exts.truncate(len); self.send(CAP(None, REQ, None, Some(exts))) } /// Sends a SASL AUTHENTICATE message with the specified data. pub fn send_sasl(&self, data: S) -> error::Result<()> { self.send(AUTHENTICATE(data.to_string())) } /// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism. pub fn send_sasl_plain(&self) -> error::Result<()> { self.send_sasl("PLAIN") } /// Sends a SASL AUTHENTICATE request to use the EXTERNAL mechanism. pub fn send_sasl_external(&self) -> error::Result<()> { self.send_sasl("EXTERNAL") } /// Sends a SASL AUTHENTICATE request to abort authentication. pub fn send_sasl_abort(&self) -> error::Result<()> { self.send_sasl("*") } /// Sends a PONG with the specified message. pub fn send_pong(&self, msg: S) -> error::Result<()> where S: fmt::Display, { self.send(PONG(msg.to_string(), None)) } /// Parts the specified channel or chanlist. pub fn send_part(&self, chanlist: S) -> error::Result<()> where S: fmt::Display, { self.send(PART(chanlist.to_string(), None)) } /// Attempts to oper up using the specified username and password. pub fn send_oper(&self, username: S1, password: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { self.send(OPER(username.to_string(), password.to_string())) } /// Sends a message to the specified target. If the message contains IRC newlines (`\r\n`), it /// will automatically be split and sent as multiple separate `PRIVMSG`s to the specified /// target. If you absolutely must avoid this behavior, you can do /// `client.send(PRIVMSG(target, message))` directly. pub fn send_privmsg(&self, target: S1, message: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { let message = message.to_string(); for line in message.split("\r\n") { self.send(PRIVMSG(target.to_string(), line.to_string()))? } Ok(()) } /// Sets the topic of a channel or requests the current one. /// If `topic` is an empty string, it won't be included in the message. pub fn send_topic(&self, channel: S1, topic: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { let topic = topic.to_string(); self.send(TOPIC( channel.to_string(), if topic.is_empty() { None } else { Some(topic) }, )) } /// Kills the target with the provided message. pub fn send_kill(&self, target: S1, message: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { self.send(KILL(target.to_string(), message.to_string())) } /// Kicks the listed nicknames from the listed channels with a comment. /// If `message` is an empty string, it won't be included in the message. pub fn send_kick( &self, chanlist: S1, nicklist: S2, message: S3, ) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, S3: fmt::Display, { let message = message.to_string(); self.send(KICK( chanlist.to_string(), nicklist.to_string(), if message.is_empty() { None } else { Some(message) }, )) } /// Changes the mode of the target by force. /// If `modeparams` is an empty string, it won't be included in the message. pub fn send_samode( &self, target: S1, mode: S2, modeparams: S3, ) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, S3: fmt::Display, { let modeparams = modeparams.to_string(); self.send(SAMODE( target.to_string(), mode.to_string(), if modeparams.is_empty() { None } else { Some(modeparams) }, )) } /// Forces a user to change from the old nickname to the new nickname. pub fn send_sanick(&self, old_nick: S1, new_nick: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { self.send(SANICK(old_nick.to_string(), new_nick.to_string())) } /// Invites a user to the specified channel. pub fn send_invite(&self, nick: S1, chan: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { self.send(INVITE(nick.to_string(), chan.to_string())) } /// Quits the server entirely with a message. /// This defaults to `Powered by Rust.` if none is specified. pub fn send_quit(&self, msg: S) -> error::Result<()> where S: fmt::Display, { let msg = msg.to_string(); self.send(QUIT(Some(if msg.is_empty() { "Powered by Rust.".to_string() } else { msg }))) } /// Sends a CTCP-escaped message to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_ctcp(&self, target: S1, msg: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { let msg = msg.to_string(); for line in msg.split("\r\n") { self.send(PRIVMSG( target.to_string(), format!("\u{001}{}\u{001}", line), ))? } Ok(()) } /// Sends an action command to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_action(&self, target: S1, msg: S2) -> error::Result<()> where S1: fmt::Display, S2: fmt::Display, { self.send_ctcp(target, &format!("ACTION {}", msg.to_string())[..]) } /// Sends a finger request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_finger(&self, target: S) -> error::Result<()> where S: fmt::Display, { self.send_ctcp(target, "FINGER") } /// Sends a version request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_version(&self, target: S) -> error::Result<()> where S: fmt::Display, { self.send_ctcp(target, "VERSION") } /// Sends a source request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_source(&self, target: S) -> error::Result<()> where S: fmt::Display, { self.send_ctcp(target, "SOURCE") } /// Sends a user info request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_user_info(&self, target: S) -> error::Result<()> where S: fmt::Display, { self.send_ctcp(target, "USERINFO") } /// Sends a finger request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_ctcp_ping(&self, target: S) -> error::Result<()> where S: fmt::Display, { let time = Local::now(); self.send_ctcp(target, &format!("PING {}", time.timestamp())[..]) } /// Sends a time request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] pub fn send_time(&self, target: S) -> error::Result<()> where S: fmt::Display, { self.send_ctcp(target, "TIME") } }; } /// A stream of `Messages` received from an IRC server via an `Client`. /// /// Interaction with this stream relies on the `futures` API, but is only expected for less /// traditional use cases. To learn more, you can view the documentation for the /// [`futures`](https://docs.rs/futures/) crate, or the tutorials for /// [`tokio`](https://tokio.rs/docs/getting-started/futures/). #[derive(Debug)] pub struct ClientStream { state: Arc, stream: SplitStream, // In case the client stream also handles outgoing messages. outgoing: Option, } impl ClientStream { /// collect stream and collect all messages available. pub async fn collect(mut self) -> error::Result> { let mut output = Vec::new(); while let Some(message) = self.next().await { match message { Ok(message) => output.push(message), Err(e) => return Err(e), } } Ok(output) } } impl FusedStream for ClientStream { fn is_terminated(&self) -> bool { false } } impl Stream for ClientStream { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if let Some(outgoing) = self.as_mut().outgoing.as_mut() { match Pin::new(outgoing).poll(cx) { Poll::Ready(Ok(())) => { // assure that we wake up again to check the incoming stream. cx.waker().wake_by_ref(); return Poll::Ready(None); } Poll::Ready(Err(e)) => { cx.waker().wake_by_ref(); return Poll::Ready(Some(Err(e))); } Poll::Pending => (), } } match ready!(Pin::new(&mut self.as_mut().stream).poll_next(cx)) { Some(Ok(msg)) => { self.state.handle_message(&msg)?; Poll::Ready(Some(Ok(msg))) } other => Poll::Ready(other), } } } /// Thread-safe internal state for an IRC server connection. #[derive(Debug)] struct ClientState { sender: Sender, /// The configuration used with this connection. config: Config, /// A thread-safe map of channels to the list of users in them. chanlists: RwLock>>, /// A thread-safe index to track the current alternative nickname being used. alt_nick_index: RwLock, /// Default ghost sequence to send if one is required but none is configured. default_ghost_sequence: Vec, } impl ClientState { fn new(sender: Sender, config: Config) -> ClientState { ClientState { sender, config, chanlists: RwLock::new(HashMap::new()), alt_nick_index: RwLock::new(0), default_ghost_sequence: vec![String::from("GHOST")], } } fn config(&self) -> &Config { &self.config } fn send>(&self, msg: M) -> error::Result<()> { let msg = msg.into(); self.handle_sent_message(&msg)?; self.sender.send(msg) } /// Gets the current nickname in use. fn current_nickname(&self) -> &str { let alt_nicks = self.config().alternate_nicknames(); let index = self.alt_nick_index.read(); match *index { 0 => self .config() .nickname() .expect("current_nickname should not be callable if nickname is not defined."), i => alt_nicks[i - 1].as_str(), } } /// Handles sent messages internally for basic client functionality. fn handle_sent_message(&self, msg: &Message) -> error::Result<()> { log::trace!("[SENT] {}", msg.to_string()); if let PART(ref chan, _) = msg.command { let _ = self.chanlists.write().remove(chan); } Ok(()) } /// Handles received messages internally for basic client functionality. fn handle_message(&self, msg: &Message) -> error::Result<()> { log::trace!("[RECV] {}", msg.to_string()); match msg.command { JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan), PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan), KICK(ref chan, ref user, _) => self.handle_part(user, chan), QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), NICK(ref new_nick) => { self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick) } ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes), PRIVMSG(ref target, ref body) => { if body.starts_with('\u{001}') { let tokens: Vec<_> = { let end = if body.ends_with('\u{001}') && body.len() > 1 { body.len() - 1 } else { body.len() }; body[1..end].split(' ').collect() }; if target.starts_with('#') { self.handle_ctcp(target, &tokens)? } else if let Some(user) = msg.source_nickname() { self.handle_ctcp(user, &tokens)? } } } Command::Response(Response::RPL_NAMREPLY, ref args) => self.handle_namreply(args), Command::Response(Response::RPL_ENDOFMOTD, _) | Command::Response(Response::ERR_NOMOTD, _) => { self.send_nick_password()?; self.send_umodes()?; let config_chans = self.config().channels(); for chan in config_chans { match self.config().channel_key(chan) { Some(key) => self.send_join_with_keys::<&str, &str>(chan, key)?, None => self.send_join(chan)?, } } let joined_chans = self.chanlists.read(); for chan in joined_chans .keys() .filter(|x| !config_chans.iter().any(|c| c == *x)) { self.send_join(chan)? } } Command::Response(Response::ERR_NICKNAMEINUSE, _) | Command::Response(Response::ERR_ERRONEOUSNICKNAME, _) => { let alt_nicks = self.config().alternate_nicknames(); let mut index = self.alt_nick_index.write(); if *index >= alt_nicks.len() { return Err(error::Error::NoUsableNick); } else { self.send(NICK(alt_nicks[*index].to_owned()))?; *index += 1; } } _ => (), } Ok(()) } fn send_nick_password(&self) -> error::Result<()> { if self.config().nick_password().is_empty() { Ok(()) } else { let mut index = self.alt_nick_index.write(); if self.config().should_ghost() && *index != 0 { let seq = match self.config().ghost_sequence() { Some(seq) => seq, None => &*self.default_ghost_sequence, }; for s in seq { self.send(NICKSERV(vec![ s.to_string(), self.config().nickname()?.to_string(), self.config().nick_password().to_string(), ]))?; } *index = 0; self.send(NICK(self.config().nickname()?.to_owned()))? } self.send(NICKSERV(vec![ "IDENTIFY".to_string(), self.config().nick_password().to_string(), ])) } } fn send_umodes(&self) -> error::Result<()> { if self.config().umodes().is_empty() { Ok(()) } else { self.send_mode( self.current_nickname(), &Mode::as_user_modes( self.config() .umodes() .split(' ') .collect::>() .as_ref(), ) .map_err(|e| error::Error::InvalidMessage { string: format!( "MODE {} {}", self.current_nickname(), self.config().umodes() ), cause: e, })?, ) } } #[cfg(not(feature = "channel-lists"))] fn handle_join(&self, _: &str, _: &str) {} #[cfg(feature = "channel-lists")] fn handle_join(&self, src: &str, chan: &str) { if let Some(vec) = self.chanlists.write().get_mut(&chan.to_owned()) { if !src.is_empty() { vec.push(User::new(src)) } } } #[cfg(not(feature = "channel-lists"))] fn handle_part(&self, _: &str, _: &str) {} #[cfg(feature = "channel-lists")] fn handle_part(&self, src: &str, chan: &str) { if let Some(vec) = self.chanlists.write().get_mut(&chan.to_owned()) { if !src.is_empty() { if let Some(n) = vec.iter().position(|x| x.get_nickname() == src) { vec.swap_remove(n); } } } } #[cfg(not(feature = "channel-lists"))] fn handle_quit(&self, _: &str) {} #[cfg(feature = "channel-lists")] fn handle_quit(&self, src: &str) { if src.is_empty() { return; } for vec in self.chanlists.write().values_mut() { if let Some(p) = vec.iter().position(|x| x.get_nickname() == src) { vec.swap_remove(p); } } } #[cfg(not(feature = "channel-lists"))] fn handle_nick_change(&self, _: &str, _: &str) {} #[cfg(feature = "channel-lists")] fn handle_nick_change(&self, old_nick: &str, new_nick: &str) { if old_nick.is_empty() || new_nick.is_empty() { return; } for (_, vec) in self.chanlists.write().iter_mut() { if let Some(n) = vec.iter().position(|x| x.get_nickname() == old_nick) { let new_entry = User::new(new_nick); vec[n] = new_entry; } } } #[cfg(not(feature = "channel-lists"))] fn handle_mode(&self, _: &str, _: &[Mode]) {} #[cfg(feature = "channel-lists")] fn handle_mode(&self, chan: &str, modes: &[Mode]) { for mode in modes { match *mode { Mode::Plus(_, Some(ref user)) | Mode::Minus(_, Some(ref user)) => { if let Some(vec) = self.chanlists.write().get_mut(chan) { if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) { vec[n].update_access_level(mode) } } } _ => (), } } } #[cfg(not(feature = "channel-lists"))] fn handle_namreply(&self, _: &[String]) {} #[cfg(feature = "channel-lists")] fn handle_namreply(&self, args: &[String]) { if args.len() == 4 { let chan = &args[2]; for user in args[3].split(' ') { self.chanlists .write() .entry(chan.clone()) .or_insert_with(Vec::new) .push(User::new(user)) } } } #[cfg(feature = "ctcp")] fn handle_ctcp(&self, resp: &str, tokens: &[&str]) -> error::Result<()> { if tokens.is_empty() { return Ok(()); } if tokens[0].eq_ignore_ascii_case("FINGER") { self.send_ctcp_internal( resp, &format!( "FINGER :{} ({})", self.config().real_name(), self.config().username() ), ) } else if tokens[0].eq_ignore_ascii_case("VERSION") { self.send_ctcp_internal(resp, &format!("VERSION {}", self.config().version())) } else if tokens[0].eq_ignore_ascii_case("SOURCE") { self.send_ctcp_internal(resp, &format!("SOURCE {}", self.config().source())) } else if tokens[0].eq_ignore_ascii_case("PING") && tokens.len() > 1 { self.send_ctcp_internal(resp, &format!("PING {}", tokens[1])) } else if tokens[0].eq_ignore_ascii_case("TIME") { self.send_ctcp_internal(resp, &format!("TIME :{}", Local::now().to_rfc2822())) } else if tokens[0].eq_ignore_ascii_case("USERINFO") { self.send_ctcp_internal(resp, &format!("USERINFO :{}", self.config().user_info())) } else { Ok(()) } } #[cfg(feature = "ctcp")] fn send_ctcp_internal(&self, target: &str, msg: &str) -> error::Result<()> { self.send_notice(target, format!("\u{001}{}\u{001}", msg)) } #[cfg(not(feature = "ctcp"))] fn handle_ctcp(&self, _: &str, _: &[&str]) -> error::Result<()> { Ok(()) } pub_state_base!(); } /// Thread-safe sender that can be used with the client. #[derive(Debug, Clone)] pub struct Sender { tx_outgoing: UnboundedSender, } impl Sender { /// Send a single message to the unbounded queue. pub fn send>(&self, msg: M) -> error::Result<()> { Ok(self.tx_outgoing.send(msg.into())?) } pub_state_base!(); pub_sender_base!(); } /// Future to handle outgoing messages. /// /// Note: this is essentially the same as a version of [SendAll](https://github.com/rust-lang-nursery/futures-rs/blob/master/futures-util/src/sink/send_all.rs) that owns it's sink and stream. #[derive(Debug)] pub struct Outgoing { sink: SplitSink, stream: UnboundedReceiver, buffered: Option, } impl Outgoing { fn try_start_send( &mut self, cx: &mut Context<'_>, message: Message, ) -> Poll> { debug_assert!(self.buffered.is_none()); match Pin::new(&mut self.sink).poll_ready(cx)? { Poll::Ready(()) => Poll::Ready(Pin::new(&mut self.sink).start_send(message)), Poll::Pending => { self.buffered = Some(message); Poll::Pending } } } } impl FusedFuture for Outgoing { fn is_terminated(&self) -> bool { // NB: outgoing stream never terminates. // TODO: should it terminate if rx_outgoing is terminated? false } } impl Future for Outgoing { type Output = error::Result<()>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = &mut *self; if let Some(message) = this.buffered.take() { ready!(this.try_start_send(cx, message))? } loop { match this.stream.poll_recv(cx) { Poll::Ready(Some(message)) => ready!(this.try_start_send(cx, message))?, Poll::Ready(None) => { ready!(Pin::new(&mut this.sink).poll_flush(cx))?; return Poll::Ready(Ok(())); } Poll::Pending => { ready!(Pin::new(&mut this.sink).poll_flush(cx))?; return Poll::Pending; } } } } } /// The canonical implementation of a connection to an IRC server. /// /// For a full example usage, see [`irc::client`]. #[derive(Debug)] pub struct Client { /// The internal, thread-safe server state. state: Arc, incoming: Option>, outgoing: Option, sender: Sender, #[cfg(test)] /// A view of the logs for a mock connection. view: Option, } impl Client { /// Creates a new `Client` from the configuration at the specified path, connecting /// immediately. This function is short-hand for loading the configuration and then calling /// `Client::from_config` and consequently inherits its behaviors. /// /// # Example /// ```no_run /// # use irc::client::prelude::*; /// # #[tokio::main] /// # async fn main() -> irc::error::Result<()> { /// let client = Client::new("config.toml").await?; /// # Ok(()) /// # } /// ``` pub async fn new>(config: P) -> error::Result { Client::from_config(Config::load(config)?).await } /// Creates a `Future` of an `Client` from the specified configuration and on the event loop /// corresponding to the given handle. This can be used to set up a number of `Clients` on a /// single, shared event loop. It can also be used to take more control over execution and error /// handling. Connection will not occur until the event loop is run. pub async fn from_config(config: Config) -> error::Result { let (tx_outgoing, rx_outgoing) = mpsc::unbounded_channel(); let conn = Connection::new(&config, tx_outgoing.clone()).await?; #[cfg(test)] let view = conn.log_view(); let (sink, incoming) = conn.split(); let sender = Sender { tx_outgoing }; Ok(Client { sender: sender.clone(), state: Arc::new(ClientState::new(sender, config)), incoming: Some(incoming), outgoing: Some(Outgoing { sink, stream: rx_outgoing, buffered: None, }), #[cfg(test)] view, }) } /// Gets the log view from the internal transport. Only used for unit testing. #[cfg(test)] fn log_view(&self) -> &self::transport::LogView { self.view .as_ref() .expect("there should be a log during testing") } /// Take the outgoing future in order to drive it yourself. /// /// Must be called before `stream` if you intend to drive this future /// yourself. pub fn outgoing(&mut self) -> Option { self.outgoing.take() } /// Get access to a thread-safe sender that can be used with the client. pub fn sender(&self) -> Sender { self.sender.clone() } /// Gets the configuration being used with this `Client`. fn config(&self) -> &Config { &self.state.config } /// Gets a stream of incoming messages from the `Client`'s connection. This is only necessary /// when trying to set up more complex clients, and requires use of the `futures` crate. Most /// You can find some examples of setups using `stream` in the /// [GitHub repository](https://github.com/aatxe/irc/tree/stable/examples). /// /// **Note**: The stream can only be returned once. Subsequent attempts will cause a panic. // FIXME: when impl traits stabilize, we should change this return type. pub fn stream(&mut self) -> error::Result { let stream = self .incoming .take() .ok_or(error::Error::StreamAlreadyConfigured)?; Ok(ClientStream { state: Arc::clone(&self.state), stream, outgoing: self.outgoing.take(), }) } /// Gets a list of currently joined channels. This will be `None` if tracking is disabled /// altogether by disabling the `channel-lists` feature. #[cfg(feature = "channel-lists")] pub fn list_channels(&self) -> Option> { Some( self.state .chanlists .read() .keys() .map(|k| k.to_owned()) .collect(), ) } /// Always returns `None` since `channel-lists` feature is disabled. #[cfg(not(feature = "channel-lists"))] pub fn list_channels(&self) -> Option> { None } /// Gets a list of [`Users`] in the specified channel. If the /// specified channel hasn't been joined or the `channel-lists` feature is disabled, this function /// will return `None`. /// /// For best results, be sure to request `multi-prefix` support from the server. This will allow /// for more accurate tracking of user rank (e.g. oper, half-op, etc.). /// # Requesting multi-prefix support /// ```no_run /// # use irc::client::prelude::*; /// use irc::proto::caps::Capability; /// /// # #[tokio::main] /// # async fn main() -> irc::error::Result<()> { /// # let client = Client::new("config.toml").await?; /// client.send_cap_req(&[Capability::MultiPrefix])?; /// client.identify()?; /// # Ok(()) /// # } /// ``` #[cfg(feature = "channel-lists")] pub fn list_users(&self, chan: &str) -> Option> { self.state.chanlists.read().get(&chan.to_owned()).cloned() } /// Always returns `None` since `channel-lists` feature is disabled. #[cfg(not(feature = "channel-lists"))] pub fn list_users(&self, _: &str) -> Option> { None } /// Gets the current nickname in use. This may be the primary username set in the configuration, /// or it could be any of the alternative nicknames listed as well. As a result, this is the /// preferred way to refer to the client's nickname. pub fn current_nickname(&self) -> &str { self.state.current_nickname() } /// Sends a [`Command`] as this `Client`. This is the /// core primitive for sending messages to the server. /// /// # Example /// ```no_run /// # use irc::client::prelude::*; /// # #[tokio::main] /// # async fn main() { /// # let client = Client::new("config.toml").await.unwrap(); /// client.send(Command::NICK("example".to_owned())).unwrap(); /// client.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); /// # } /// ``` pub fn send>(&self, msg: M) -> error::Result<()> { self.state.send(msg) } /// Sends a CAP END, NICK and USER to identify. pub fn identify(&self) -> error::Result<()> { // Send a CAP END to signify that we're IRCv3-compliant (and to end negotiations!). self.send(CAP(None, END, None, None))?; if self.config().password() != "" { self.send(PASS(self.config().password().to_owned()))?; } self.send(NICK(self.config().nickname()?.to_owned()))?; self.send(USER( self.config().username().to_owned(), "0".to_owned(), self.config().real_name().to_owned(), ))?; Ok(()) } pub_state_base!(); pub_sender_base!(); } #[cfg(test)] mod test { use std::{collections::HashMap, default::Default, thread, time::Duration}; use super::Client; #[cfg(feature = "channel-lists")] use crate::client::data::User; use crate::{ client::data::Config, error::Error, proto::{ command::Command::{Raw, PRIVMSG}, ChannelMode, IrcCodec, Mode, }, }; use anyhow::Result; use futures::prelude::*; pub fn test_config() -> Config { Config { owners: vec!["test".to_string()], nickname: Some("test".to_string()), alt_nicks: vec!["test2".to_string()], server: Some("irc.test.net".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], user_info: Some("Testing.".to_string()), use_mock_connection: true, ..Default::default() } } pub fn get_client_value(client: Client) -> String { // We sleep here because of synchronization issues. // We can't guarantee that everything will have been sent by the time of this call. thread::sleep(Duration::from_millis(100)); client .log_view() .sent() .unwrap() .iter() .fold(String::new(), |mut acc, msg| { // NOTE: we have to sanitize here because sanitization happens in IrcCodec after the // messages are converted into strings, but our transport logger catches messages before // they ever reach that point. acc.push_str(&IrcCodec::sanitize(msg.to_string())); acc }) } #[tokio::test] async fn stream() -> Result<()> { let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\ :test!test@test JOIN #test\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(exp.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; // assert_eq!(&messages[..], exp); Ok(()) } #[tokio::test] async fn handle_message() -> Result<()> { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "JOIN #test\r\nJOIN #test2\r\n" ); Ok(()) } #[tokio::test] async fn handle_end_motd_with_nick_password() -> Result<()> { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nick_password: Some("password".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NICKSERV IDENTIFY password\r\nJOIN #test\r\n\ JOIN #test2\r\n" ); Ok(()) } #[tokio::test] async fn handle_end_motd_with_chan_keys() -> Result<()> { let value = ":irc.test.net 376 test :End of /MOTD command\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some("test".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], channel_keys: { let mut map = HashMap::new(); map.insert("#test2".to_string(), "password".to_string()); map }, ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "JOIN #test\r\nJOIN #test2 password\r\n" ); Ok(()) } #[tokio::test] async fn handle_end_motd_with_ghost() -> Result<()> { let value = ":irc.test.net 433 * test :Nickname is already in use.\r\n\ :irc.test.net 376 test2 :End of /MOTD command.\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some("test".to_string()), alt_nicks: vec!["test2".to_string()], nick_password: Some("password".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], should_ghost: true, ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NICK test2\r\nNICKSERV GHOST test password\r\n\ NICK test\r\nNICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n" ); Ok(()) } #[tokio::test] async fn handle_end_motd_with_ghost_seq() -> Result<()> { let value = ":irc.test.net 433 * test :Nickname is already in use.\r\n\ :irc.test.net 376 test2 :End of /MOTD command.\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some("test".to_string()), alt_nicks: vec!["test2".to_string()], nick_password: Some("password".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], should_ghost: true, ghost_sequence: Some(vec!["RECOVER".to_string(), "RELEASE".to_string()]), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NICK test2\r\nNICKSERV RECOVER test password\ \r\nNICKSERV RELEASE test password\r\nNICK test\r\nNICKSERV IDENTIFY password\ \r\nJOIN #test\r\nJOIN #test2\r\n" ); Ok(()) } #[tokio::test] async fn handle_end_motd_with_umodes() -> Result<()> { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some("test".to_string()), umodes: Some("+B".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n" ); Ok(()) } #[tokio::test] async fn nickname_in_use() -> Result<()> { let value = ":irc.test.net 433 * test :Nickname is already in use.\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "NICK test2\r\n"); Ok(()) } #[tokio::test] async fn ran_out_of_nicknames() -> Result<()> { let value = ":irc.test.net 433 * test :Nickname is already in use.\r\n\ :irc.test.net 433 * test2 :Nickname is already in use.\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; let res = client.stream()?.try_collect::>().await; if let Err(Error::NoUsableNick) = res { } else { panic!("expected error when no valid nicks were specified") } Ok(()) } #[tokio::test] async fn send() -> Result<()> { let mut client = Client::from_config(test_config()).await?; assert!(client .send(PRIVMSG("#test".to_string(), "Hi there!".to_string())) .is_ok()); client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n" ); Ok(()) } #[tokio::test] async fn send_no_newline_injection() -> Result<()> { let mut client = Client::from_config(test_config()).await?; assert!(client .send(PRIVMSG( "#test".to_string(), "Hi there!\r\nJOIN #bad".to_string() )) .is_ok()); client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n" ); Ok(()) } #[tokio::test] async fn send_raw_is_really_raw() -> Result<()> { let mut client = Client::from_config(test_config()).await?; assert!(client .send(Raw("PASS".to_owned(), vec!["password".to_owned()])) .is_ok()); assert!(client .send(Raw("NICK".to_owned(), vec!["test".to_owned()])) .is_ok()); client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PASS password\r\nNICK test\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "channel-lists")] async fn channel_tracking_names() -> Result<()> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!(client.list_channels().unwrap(), vec!["#test".to_owned()]); Ok(()) } #[tokio::test] #[cfg(feature = "channel-lists")] async fn channel_tracking_names_part() -> Result<()> { use crate::proto::command::Command::PART; let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!(client.list_channels(), Some(vec!["#test".to_owned()])); // we ignore the result, as soon as we queue an outgoing message we // update client state, regardless if the queue is available or not. let _ = client.send(PART("#test".to_string(), None)); assert_eq!(client.list_channels(), Some(vec![])); Ok(()) } #[tokio::test] #[cfg(feature = "channel-lists")] async fn user_tracking_names() -> Result<()> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![User::new("test"), User::new("~owner"), User::new("&admin")] ); Ok(()) } #[tokio::test] #[cfg(feature = "channel-lists")] async fn user_tracking_names_join() -> Result<()> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ :test2!test@test JOIN #test\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![ User::new("test"), User::new("~owner"), User::new("&admin"), User::new("test2"), ] ); Ok(()) } #[tokio::test] #[cfg(feature = "channel-lists")] async fn user_tracking_names_kick() -> Result<()> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ :owner!test@test KICK #test test\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![User::new("&admin"), User::new("~owner"),] ); Ok(()) } #[tokio::test] #[cfg(feature = "channel-lists")] async fn user_tracking_names_part() -> Result<()> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ :owner!test@test PART #test\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![User::new("test"), User::new("&admin")] ); Ok(()) } #[tokio::test] #[cfg(feature = "channel-lists")] async fn user_tracking_names_mode() -> Result<()> { let value = ":irc.test.net 353 test = #test :+test ~owner &admin\r\n\ :test!test@test MODE #test +o test\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![User::new("@test"), User::new("~owner"), User::new("&admin")] ); let mut exp = User::new("@test"); exp.update_access_level(&Mode::Plus(ChannelMode::Voice, None)); assert_eq!( client.list_users("#test").unwrap()[0].highest_access_level(), exp.highest_access_level() ); // The following tests if the maintained user contains the same entries as what is expected // but ignores the ordering of these entries. let mut levels = client.list_users("#test").unwrap()[0].access_levels(); levels.retain(|l| exp.access_levels().contains(l)); assert_eq!(levels.len(), exp.access_levels().len()); Ok(()) } #[tokio::test] #[cfg(not(feature = "channel-lists"))] async fn no_user_tracking() -> Result<()> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert!(client.list_users("#test").is_none()); Ok(()) } #[tokio::test] async fn handle_single_soh() -> Result<()> { let value = ":test!test@test PRIVMSG #test :\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some("test".to_string()), channels: vec!["#test".to_string(), "#test2".to_string()], ..test_config() }) .await?; client.stream()?.collect().await?; Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn finger_response() -> Result<()> { let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}FINGER :test (test)\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn version_response() -> Result<()> { let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], &format!( "NOTICE test :\u{001}VERSION {}\u{001}\r\n", crate::VERSION_STR, ) ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn source_response() -> Result<()> { let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn ctcp_ping_response() -> Result<()> { let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}PING test\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn time_response() -> Result<()> { let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; let val = get_client_value(client); assert!(val.starts_with("NOTICE test :\u{001}TIME :")); assert!(val.ends_with("\u{001}\r\n")); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn user_info_response() -> Result<()> { let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}USERINFO :Testing.\u{001}\ \r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn ctcp_ping_no_timestamp() -> Result<()> { let value = ":test!test@test PRIVMSG test \u{001}PING\u{001}\r\n"; let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }) .await?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], ""); Ok(()) } #[tokio::test] async fn identify() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.identify()?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "CAP END\r\nNICK test\r\n\ USER test 0 * test\r\n" ); Ok(()) } #[tokio::test] async fn identify_with_password() -> Result<()> { let mut client = Client::from_config(Config { nickname: Some("test".to_string()), password: Some("password".to_string()), ..test_config() }) .await?; client.identify()?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "CAP END\r\nPASS password\r\nNICK test\r\n\ USER test 0 * test\r\n" ); Ok(()) } #[tokio::test] async fn send_pong() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_pong("irc.test.net")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "PONG irc.test.net\r\n"); Ok(()) } #[tokio::test] async fn send_join() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_join("#test,#test2,#test3")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "JOIN #test,#test2,#test3\r\n" ); Ok(()) } #[tokio::test] async fn send_part() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_part("#test")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "PART #test\r\n"); Ok(()) } #[tokio::test] async fn send_oper() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_oper("test", "test")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "OPER test test\r\n"); Ok(()) } #[tokio::test] async fn send_privmsg() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_privmsg("#test", "Hi, everybody!")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG #test :Hi, everybody!\r\n" ); Ok(()) } #[tokio::test] async fn send_notice() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_notice("#test", "Hi, everybody!")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE #test :Hi, everybody!\r\n" ); Ok(()) } #[tokio::test] async fn send_topic_no_topic() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_topic("#test", "")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n"); Ok(()) } #[tokio::test] async fn send_topic() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_topic("#test", "Testing stuff.")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "TOPIC #test :Testing stuff.\r\n" ); Ok(()) } #[tokio::test] async fn send_kill() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_kill("test", "Testing kills.")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "KILL test :Testing kills.\r\n" ); Ok(()) } #[tokio::test] async fn send_kick_no_message() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_kick("#test", "test", "")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n"); Ok(()) } #[tokio::test] async fn send_kick() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_kick("#test", "test", "Testing kicks.")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "KICK #test test :Testing kicks.\r\n" ); Ok(()) } #[tokio::test] async fn send_mode_no_modeparams() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)])?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n"); Ok(()) } #[tokio::test] async fn send_mode() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_mode( "#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))], )?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n"); Ok(()) } #[tokio::test] async fn send_samode_no_modeparams() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_samode("#test", "+i", "")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n"); Ok(()) } #[tokio::test] async fn send_samode() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_samode("#test", "+o", "test")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n"); Ok(()) } #[tokio::test] async fn send_sanick() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_sanick("test", "test2")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n"); Ok(()) } #[tokio::test] async fn send_invite() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_invite("test", "#test")?; client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n"); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_ctcp() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_ctcp("test", "LINE1\r\nLINE2\r\nLINE3")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG test \u{001}LINE1\u{001}\r\nPRIVMSG test \u{001}LINE2\u{001}\r\nPRIVMSG test \u{001}LINE3\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_action() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_action("test", "tests.")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_finger() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_finger("test")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG test \u{001}FINGER\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_version() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_version("test")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG test \u{001}VERSION\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_source() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_source("test")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG test \u{001}SOURCE\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_user_info() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_user_info("test")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG test \u{001}USERINFO\u{001}\r\n" ); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_ctcp_ping() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_ctcp_ping("test")?; client.stream()?.collect().await?; let val = get_client_value(client); println!("{}", val); assert!(val.starts_with("PRIVMSG test :\u{001}PING ")); assert!(val.ends_with("\u{001}\r\n")); Ok(()) } #[tokio::test] #[cfg(feature = "ctcp")] async fn send_time() -> Result<()> { let mut client = Client::from_config(test_config()).await?; client.send_time("test")?; client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG test \u{001}TIME\u{001}\r\n" ); Ok(()) } } irc-1.0.0/src/client/prelude.rs000064400000000000000000000037351046102023000144620ustar 00000000000000//! A client-side IRC prelude, re-exporting the complete high-level IRC client API. //! //! # Structure //! A connection to an IRC server is created via an `IrcClient` which is configured using a //! `Config` struct that defines data such as which server to connect to, on what port, and //! using what nickname. The `Client` trait provides an API for actually interacting with the //! server once a connection has been established. This API intentionally offers only a single //! method to send `Commands` because it makes it easy to see the whole set of possible //! interactions with a server. The `ClientExt` trait addresses this deficiency by defining a //! number of methods that provide a more clear and succinct interface for sending various //! common IRC commands to the server. An `IrcReactor` can be used to create and manage multiple //! `IrcClients` with more fine-grained control over error management. //! //! The various `proto` types capture details of the IRC protocol that are used throughout the //! client API. `Message`, `Command`, and `Response` are used to send and receive messages along //! the connection, and are naturally at the heart of communication in the IRC protocol. //! `Capability` and `NegotiationVersion` are used to determine (with the server) what IRCv3 //! functionality to enable for the connection. Certain parts of the API offer suggestions for //! extensions that will improve the user experience, and give examples of how to enable them //! using `Capability`. `Mode`, `ChannelMode`, and `UserMode` are used in a high-level API for //! dealing with IRC channel and user modes. They appear in methods for sending mode commands, //! as well as in the parsed form of received mode commands. #[cfg(feature = "proxy")] pub use crate::client::data::ProxyType; pub use crate::{ client::{data::Config, Client, Sender}, proto::{ Capability, ChannelExt, ChannelMode, Command, Message, Mode, NegotiationVersion, Prefix, Response, UserMode, }, }; irc-1.0.0/src/client/transport.rs000064400000000000000000000231501046102023000150470ustar 00000000000000//! An IRC transport that wraps an IRC-framed stream to provide a number of features including //! automatic PING replies, automatic sending of PINGs, and message rate-limiting. This can be used //! as the basis for implementing a more full IRC client. use std::{ pin::Pin, sync::{Arc, RwLock, RwLockReadGuard}, task::{Context, Poll}, time::Duration, }; use chrono::prelude::*; use futures_util::{future::Future, ready, sink::Sink, stream::Stream}; use pin_project::pin_project; use tokio::sync::mpsc::UnboundedSender; use tokio::{ io::{AsyncRead, AsyncWrite}, time::{self, Interval, Sleep}, }; use tokio_util::codec::Framed; use crate::{ client::data::Config, error, proto::{Command, IrcCodec, Message, Response}, }; /// Pinger-based futures helper. #[pin_project] struct Pinger { tx: UnboundedSender, // Whether this pinger pings. enabled: bool, /// The amount of time to wait before timing out from no ping response. ping_timeout: Duration, /// The instant that the last ping was sent to the server. #[pin] ping_deadline: Option, /// The interval at which to send pings. #[pin] ping_interval: Interval, } impl Pinger { /// Construct a new pinger helper. pub fn new(tx: UnboundedSender, config: &Config) -> Pinger { let ping_time = Duration::from_secs(u64::from(config.ping_time())); let ping_timeout = Duration::from_secs(u64::from(config.ping_timeout())); Self { tx, enabled: false, ping_timeout, ping_deadline: None, ping_interval: time::interval(ping_time), } } /// Handle an incoming message. fn handle_message(self: Pin<&mut Self>, message: &Message) -> error::Result<()> { match message.command { Command::Response(Response::RPL_ENDOFMOTD, _) | Command::Response(Response::ERR_NOMOTD, _) => { *self.project().enabled = true; } // On receiving a `PING` message from the server, we automatically respond with // the appropriate `PONG` message to keep the connection alive for transport. Command::PING(ref data, _) => { self.send_pong(data)?; } // Check `PONG` responses from the server. If it matches, we will update the // last instant that the pong was received. This will prevent timeout. Command::PONG(_, None) | Command::PONG(_, Some(_)) => { log::trace!("Received PONG"); self.project().ping_deadline.set(None); } _ => (), } Ok(()) } /// Send a pong. fn send_pong(self: Pin<&mut Self>, data: &str) -> error::Result<()> { self.project() .tx .send(Command::PONG(data.to_owned(), None).into())?; Ok(()) } /// Sends a ping via the transport. fn send_ping(self: Pin<&mut Self>) -> error::Result<()> { log::trace!("Sending PING"); // Creates new ping data using the local timestamp. let data = format!("{}", Local::now().timestamp()); let mut this = self.project(); this.tx.send(Command::PING(data, None).into())?; if this.ping_deadline.is_none() { let ping_deadline = time::sleep(*this.ping_timeout); this.ping_deadline.set(Some(ping_deadline)); } Ok(()) } } impl Future for Pinger { type Output = Result<(), error::Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { if let Some(ping_deadline) = self.as_mut().project().ping_deadline.as_pin_mut() { match ping_deadline.poll(cx) { Poll::Ready(()) => return Poll::Ready(Err(error::Error::PingTimeout)), Poll::Pending => (), } } if self .as_mut() .project() .ping_interval .poll_tick(cx) .is_ready() && *self.as_mut().project().enabled { self.as_mut().send_ping()?; } Poll::Pending } } /// An IRC transport that handles core functionality for the IRC protocol. This is used in the /// implementation of `Connection` and ultimately `IrcServer`, and plays an important role in /// handling connection timeouts, message throttling, and ping response. #[pin_project] pub struct Transport { /// The inner connection framed with an `IrcCodec`. #[pin] inner: Framed, /// Helper for handle pinging. #[pin] pinger: Option, } impl Transport where T: Unpin + AsyncRead + AsyncWrite, { /// Creates a new `Transport` from the given IRC stream. pub fn new( config: &Config, inner: Framed, tx: UnboundedSender, ) -> Transport { let pinger = Some(Pinger::new(tx, config)); Transport { inner, pinger } } /// Gets the inner stream underlying the `Transport`. pub fn into_inner(self) -> Framed { self.inner } } impl Stream for Transport where T: Unpin + AsyncRead + AsyncWrite, { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if let Some(pinger) = self.as_mut().project().pinger.as_pin_mut() { match pinger.poll(cx) { Poll::Ready(result) => result?, Poll::Pending => (), } } let result = ready!(self.as_mut().project().inner.poll_next(cx)); let message = match result { None => return Poll::Ready(None), Some(message) => message?, }; if let Some(pinger) = self.as_mut().project().pinger.as_pin_mut() { pinger.handle_message(&message)?; } Poll::Ready(Some(Ok(message))) } } impl Sink for Transport where T: Unpin + AsyncRead + AsyncWrite, { type Error = error::Error; fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { ready!(self.project().inner.poll_ready(cx))?; Poll::Ready(Ok(())) } fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { log::trace!("[SEND] {}", item); self.project().inner.start_send(item)?; Ok(()) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { ready!(self.project().inner.poll_flush(cx))?; Poll::Ready(Ok(())) } fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { ready!(self.project().inner.poll_close(cx))?; Poll::Ready(Ok(())) } } /// A view of the logs from a particular `Logged` transport. #[derive(Clone, Debug)] pub struct LogView { sent: Arc>>, received: Arc>>, } impl LogView { /// Gets a read guard for all the messages sent on the transport. pub fn sent(&self) -> error::Result>> { self.sent.read().map_err(|_| error::Error::PoisonedLog) } /// Gets a read guard for all the messages received on the transport. pub fn received(&self) -> error::Result>> { self.received.read().map_err(|_| error::Error::PoisonedLog) } } /// A logged version of the `Transport` that records all sent and received messages. /// Note: this will introduce some performance overhead by cloning all messages. #[pin_project] pub struct Logged { #[pin] inner: Transport, view: LogView, } impl Logged where T: AsyncRead + AsyncWrite, { /// Wraps the given `Transport` in logging. pub fn wrap(inner: Transport) -> Logged { Logged { inner, view: LogView { sent: Arc::new(RwLock::new(vec![])), received: Arc::new(RwLock::new(vec![])), }, } } /// Gets a view of the logging for this transport. pub fn view(&self) -> LogView { self.view.clone() } } impl Stream for Logged where T: Unpin + AsyncRead + AsyncWrite, { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.project(); match ready!(this.inner.poll_next(cx)) { Some(msg) => { let msg = msg?; this.view .received .write() .map_err(|_| error::Error::PoisonedLog)? .push(msg.clone()); Poll::Ready(Some(Ok(msg))) } None => Poll::Ready(None), } } } impl Sink for Logged where T: Unpin + AsyncRead + AsyncWrite, { type Error = error::Error; fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.project().inner.poll_ready(cx) } fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.project().inner.poll_close(cx) } fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { let this = self.project(); this.inner.start_send(item.clone())?; this.view .sent .write() .map_err(|_| error::Error::PoisonedLog)? .push(item); Ok(()) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.project().inner.poll_flush(cx) } } irc-1.0.0/src/error.rs000064400000000000000000000132361046102023000126720ustar 00000000000000//! Errors for `irc` crate using `failure`. use std::io::Error as IoError; use std::sync::mpsc::RecvError; use thiserror::Error; use tokio::sync::mpsc::error::{SendError, TrySendError}; #[cfg(feature = "tls-rust")] use tokio_rustls::rustls::client::InvalidDnsNameError; use crate::proto::error::{MessageParseError, ProtocolError}; /// A specialized `Result` type for the `irc` crate. pub type Result = std::result::Result; /// The main crate-wide error type. #[derive(Debug, Error)] pub enum Error { /// An internal I/O error. #[error("an io error occurred")] Io( #[source] #[from] IoError, ), /// An internal proxy error. #[cfg(feature = "proxy")] #[error("a proxy error occurred")] Proxy(#[from] tokio_socks::Error), /// An internal TLS error. #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] #[error("a TLS error occurred: {0}")] Tls( #[source] #[from] native_tls::Error, ), /// An internal TLS error. #[cfg(feature = "tls-rust")] #[error("a TLS error occurred")] Tls( #[source] #[from] tokio_rustls::rustls::Error, ), /// An invalid DNS name was specified. #[cfg(feature = "tls-rust")] #[error("invalid DNS name")] InvalidDnsNameError( #[source] #[from] InvalidDnsNameError, ), /// An internal synchronous channel closed. #[error("a sync channel closed")] SyncChannelClosed( #[source] #[from] RecvError, ), /// An internal asynchronous channel closed. #[error("an async channel closed")] AsyncChannelClosed, /// An internal oneshot channel closed. #[error("a oneshot channel closed")] OneShotCanceled, /// Error for invalid configurations. #[error("invalid config: {}", path)] InvalidConfig { /// The path to the configuration, or "" if none specified. path: String, /// The detailed configuration error. #[source] cause: ConfigError, }, /// Error for invalid messages. #[error("invalid message: {}", string)] InvalidMessage { /// The string that failed to parse. string: String, /// The detailed message parsing error. #[source] cause: MessageParseError, }, /// Mutex for a logged transport was poisoned making the log inaccessible. #[error("mutex for a logged transport was poisoned")] PoisonedLog, /// Ping timed out due to no response. #[error("connection reset: no ping response")] PingTimeout, /// Failed to lookup an unknown codec. #[error("unknown codec: {}", codec)] UnknownCodec { /// The attempted codec. codec: String, }, /// Failed to encode or decode something with the given codec. #[error("codec {} failed: {}", codec, data)] CodecFailed { /// The canonical codec name. codec: &'static str, /// The data that failed to encode or decode. data: String, }, /// All specified nicknames were in use or unusable. #[error("none of the specified nicknames were usable")] NoUsableNick, /// Stream has already been configured. #[error("stream has already been configured")] StreamAlreadyConfigured, } /// Errors that occur with configurations. #[derive(Debug, Error)] pub enum ConfigError { /// Failed to parse as TOML. #[cfg(feature = "toml_config")] #[error("invalid toml")] InvalidToml(#[source] TomlError), /// Failed to parse as JSON. #[cfg(feature = "json_config")] #[error("invalid json")] InvalidJson(#[source] serde_json::Error), /// Failed to parse as YAML. #[cfg(feature = "yaml_config")] #[error("invalid yaml")] InvalidYaml(#[source] serde_yaml::Error), /// Failed to parse the given format because it was disabled at compile-time. #[error("config format disabled: {}", format)] ConfigFormatDisabled { /// The disabled file format. format: &'static str, }, /// Could not identify the given file format. #[error("config format unknown: {}", format)] UnknownConfigFormat { /// The unknown file extension. format: String, }, /// File was missing an extension to identify file format. #[error("missing format extension")] MissingExtension, /// Configuration does not specify a nickname. #[error("nickname not specified")] NicknameNotSpecified, /// Configuration does not specify a server. #[error("server not specified")] ServerNotSpecified, /// The specified file could not be read. #[error("could not read file {}", file)] FileMissing { /// The supposed location of the file. file: String, }, } /// A wrapper that combines toml's serialization and deserialization errors. #[cfg(feature = "toml_config")] #[derive(Debug, Error)] pub enum TomlError { /// A TOML deserialization error. #[error("deserialization failed")] Read(#[source] toml::de::Error), /// A TOML serialization error. #[error("serialization failed")] Write(#[source] toml::ser::Error), } impl From for Error { fn from(e: ProtocolError) -> Error { match e { ProtocolError::Io(e) => Error::Io(e), ProtocolError::InvalidMessage { string, cause } => { Error::InvalidMessage { string, cause } } } } } impl From> for Error { fn from(_: SendError) -> Error { Error::AsyncChannelClosed } } impl From> for Error { fn from(_: TrySendError) -> Error { Error::AsyncChannelClosed } } irc-1.0.0/src/lib.rs000064400000000000000000000036621046102023000123110ustar 00000000000000//! A simple, thread-safe, and async-friendly library for IRC clients. //! //! # Quick Start //! The main public API is entirely exported in [`client::prelude`]. //! This should include everything necessary to write an IRC client or bot. //! //! # A Whirlwind Tour //! The irc crate is divided into two main modules: [`client`] and //! [`proto`]. As the names suggest, the `client` module captures the whole of //! the client-side functionality, while the `proto` module features general components of an IRC //! protocol implementation that could in principle be used in either client or server software. //! Both modules feature a number of components that are low-level and can be used to build //! alternative APIs for the IRC protocol. For the average user, the higher-level components for an //! IRC client are all re-exported in [`client::prelude`]. That module //! serves as the best starting point for a new user trying to understand the high-level API. //! //! # Example //! //! ```no_run //! use irc::client::prelude::*; //! use futures::prelude::*; //! //! # #[tokio::main] //! # async fn main() -> irc::error::Result<()> { //! // configuration is loaded from config.toml into a Config //! let mut client = Client::new("config.toml").await?; //! // identify comes from ClientExt //! client.identify()?; //! //! let mut stream = client.stream()?; //! //! while let Some(message) = stream.next().await.transpose()? { //! if let Command::PRIVMSG(channel, message) = message.command { //! if message.contains(&*client.current_nickname()) { //! // send_privmsg comes from ClientExt //! client.send_privmsg(&channel, "beep boop").unwrap(); //! } //! } //! } //! # Ok(()) //! # } //! ``` #![warn(missing_docs)] pub extern crate irc_proto as proto; pub mod client; pub mod error; const VERSION_STR: &str = concat!( env!("CARGO_PKG_NAME"), ":", env!("CARGO_PKG_VERSION"), ":Compiled with rustc", );